Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e17ce5504e | ||
|
|
d1492f3b44 | ||
|
|
29176eb90b | ||
|
|
75bb730d3c | ||
|
|
100005379b | ||
|
|
c71aca637c | ||
|
|
5b8ee4f4ad | ||
|
|
821e1c3e2f | ||
|
|
bff609097b | ||
|
|
e9329ccbb2 | ||
|
|
b798b3f9b7 | ||
|
|
25fa51c76d | ||
|
|
7f60661eef |
No files matched your search
@@ -0,0 +1,158 @@
|
||||
name: Test, build and publish
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: '0'
|
||||
CARGO_PROFILE_DEV_DEBUG: '0'
|
||||
CARGO_PROFILE_TEST_DEBUG: '0'
|
||||
RUST_BACKTRACE: '1'
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: All tests and Linux binary
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install native test dependencies and make room for 10 GiB tests
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends build-essential pkg-config libssl-dev libicu-dev lua5.4 liblua5.4-dev lua-filesystem lua-expat lua-socket lua-sec openssl
|
||||
rust_version=$(python3 -c 'import tomllib; print(tomllib.load(open("rust-toolchain.toml", "rb"))["toolchain"]["channel"])')
|
||||
rustup toolchain install "$rust_version" --profile minimal --component rustfmt --component clippy
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24.3'
|
||||
cache: false
|
||||
- name: Build exact native Prosody reference
|
||||
run: bash scripts/ci/prepare-reference.sh "$RUNNER_TEMP/reference"
|
||||
- name: Build genuine MinIO
|
||||
run: |
|
||||
bash scripts/ci/prepare-minio.sh "$RUNNER_TEMP/minio"
|
||||
go clean -cache -modcache
|
||||
df -h . "$RUNNER_TEMP"
|
||||
python3 -c 'import shutil; assert shutil.disk_usage(".").free >= 20 * 2**30, "Need 20 GiB free for actual upload tests"'
|
||||
- name: Run every test, native oracle, load and real 10 GiB uploads
|
||||
env:
|
||||
PROSODY_REFERENCE_SOURCE: ${{ runner.temp }}/reference/prosody-cafd74b9c5ea
|
||||
PROSODY_REFERENCE_LUA: lua5.4
|
||||
MINIO_BINARY: ${{ runner.temp }}/minio
|
||||
run: |
|
||||
mkdir -p ci-results
|
||||
set -o pipefail
|
||||
bash scripts/ci/verify-all.sh 2>&1 | tee ci-results/verify.log
|
||||
- name: Package tested binary
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cp target/release/rust-xmpp dist/rust-xmpp
|
||||
chmod 0755 dist/rust-xmpp
|
||||
dist/rust-xmpp --version
|
||||
(cd dist && sha256sum rust-xmpp > SHA256SUMS)
|
||||
- name: Save binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rust-xmpp-linux-amd64
|
||||
path: dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
- name: Save test reports even on failure
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results
|
||||
path: |
|
||||
ci-results/
|
||||
/tmp/muc-sm-load.json
|
||||
/tmp/xmpp-message-latency.json
|
||||
/tmp/personal-message-latency.json
|
||||
/tmp/push-latency.json
|
||||
/tmp/push-offline-latency.json
|
||||
/tmp/bind2-archive-latency.json
|
||||
/tmp/slow-storage-latency.json
|
||||
/tmp/slow-storage-crash.json
|
||||
/tmp/journal-recovery-sync.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 14
|
||||
|
||||
image:
|
||||
name: Container smoke test and Gitea publish
|
||||
needs: verify
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
# Only the publication job needs registry credentials. PRs still build and test images.
|
||||
env:
|
||||
GITEA_REGISTRY: ${{ vars.GITEA_REGISTRY }}
|
||||
GITEA_IMAGE: ${{ vars.GITEA_IMAGE }}
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
PUBLISH: ${{ github.event_name != 'pull_request' && (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || startsWith(github.ref, 'refs/tags/v')) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rust-xmpp-linux-amd64
|
||||
path: dist
|
||||
- name: Verify downloaded executable
|
||||
run: |
|
||||
(cd dist && sha256sum --check SHA256SUMS)
|
||||
chmod 0755 dist/rust-xmpp
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: Build exact tested binary into runtime image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
target: ci-runtime
|
||||
platforms: linux/amd64
|
||||
load: true
|
||||
push: false
|
||||
tags: rust-xmpp:ci
|
||||
- name: Smoke-test runtime image
|
||||
run: |
|
||||
docker run --rm rust-xmpp:ci --version
|
||||
docker run --rm --entrypoint /usr/local/bin/prosodyctl rust-xmpp:ci --version
|
||||
id=$(docker create rust-xmpp:ci)
|
||||
trap 'docker rm "$id" >/dev/null' EXIT
|
||||
docker cp "$id:/usr/local/bin/rust-xmpp" /tmp/container-rust-xmpp
|
||||
cmp dist/rust-xmpp /tmp/container-rust-xmpp
|
||||
- name: Validate publication configuration
|
||||
if: env.PUBLISH == 'true'
|
||||
env:
|
||||
REGISTRY_USER: ${{ secrets.GITEA_REGISTRY_USERNAME }}
|
||||
REGISTRY_TOKEN: ${{ secrets.GITEA_REGISTRY_TOKEN }}
|
||||
run: python3 scripts/ci/image-tags.py
|
||||
id: image
|
||||
- name: Login to private Gitea registry
|
||||
if: env.PUBLISH == 'true'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ vars.GITEA_REGISTRY }}
|
||||
username: ${{ secrets.GITEA_REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.GITEA_REGISTRY_TOKEN }}
|
||||
- name: Push the smoke-tested image
|
||||
if: env.PUBLISH == 'true'
|
||||
env:
|
||||
IMAGE_TAGS: ${{ steps.image.outputs.tags }}
|
||||
run: |
|
||||
while IFS= read -r tag; do
|
||||
docker tag rust-xmpp:ci "$tag"
|
||||
docker push "$tag"
|
||||
printf 'Published `%s`\n' "$tag" >> "$GITHUB_STEP_SUMMARY"
|
||||
done <<< "$IMAGE_TAGS"
|
||||
@@ -2,3 +2,4 @@
|
||||
*.local.*
|
||||
/demo/
|
||||
/bin/
|
||||
**pycache**
|
||||
@@ -0,0 +1,347 @@
|
||||
# Продолжение разработки Rust XMPP
|
||||
|
||||
## Цель и правила работы
|
||||
|
||||
Пользователь хочет заменить свою установку Prosody этим сервером на Rust.
|
||||
Продолжайте реализацию и исправления самостоятельно в рамках текущего запроса:
|
||||
воспроизводите проблему, исправляйте причину, проверяйте результат, обновляйте
|
||||
документацию. Не ограничивайтесь планом, если доступна реализация.
|
||||
|
||||
- Общайтесь с пользователем по-русски, коротко сообщайте существенные результаты.
|
||||
- Пользователь разрешил субагентов. Передавайте короткий самостоятельный brief,
|
||||
`fork_turns: "none"`, путь проекта, конкретную задачу, ограничения и проверку.
|
||||
Назначайте отдельные файлы владельцам; не допускайте конкурирующих правок.
|
||||
- Сначала изучите актуальные файлы и незавершённые изменения. Не откатывайте
|
||||
чужую работу и не считайте предыдущую среду или набор инструментов неизменными.
|
||||
- Не объявляйте полную совместимость с Prosody по результату ограниченных тестов.
|
||||
Сохраняйте `--experimental`, пока не выполнены критерии COMPATIBILITY.md.
|
||||
- Не подменяйте реализацию рекламой disco features. Проверяйте реальный wire flow.
|
||||
- Не пишите passwords, SASL payload, tokens, private keys в логи/отчёты/репозиторий.
|
||||
- Production-конфигурацию, DNS и внешний registry меняйте только в рамках
|
||||
соответствующего задания и доступных полномочий. Не выдавайте локальные
|
||||
fixture-проверки за проверку реального клиента или production-сервера.
|
||||
|
||||
## Где находится проект
|
||||
|
||||
Корень — каталог с этим AGENTS.md, Cargo.toml и Dockerfile. В текущем workspace
|
||||
это `/workspace/scratch/362577cdf2e0/recovered/rust-xmpp`; путь непереносимый.
|
||||
В нём сейчас нет `.git`. Если следующая среда содержит Git checkout, используйте
|
||||
его состояние и инструкции, не создавайте второй расходящийся источник истины.
|
||||
|
||||
Версия Rust source — **0.8.3**. В0.8.0 добавлены durable MUC/SM, затем CI
|
||||
и этот handoff;0.8.1 исправляет задержку сообщений. См. docs/MESSAGE-LATENCY.md.
|
||||
Перед передачей исходники упаковываются в ZIP; актуальный архив должен содержать
|
||||
этот файл и `.github/workflows/ci.yml`. В среде ChatGPT сохраняйте новый архив
|
||||
через доступный Library workflow, обновляя существующий проект, если его identity
|
||||
известна. Не считайте scratch единственным долговременным экземпляром проекта.
|
||||
|
||||
## С чего начать
|
||||
|
||||
1. Прочитайте README.md, docs/COMPATIBILITY.md, docs/VALIDATION.md.
|
||||
2. Для MUC/SM: docs/MUC-RECOVERY.md, CHANGELOG-0.8.0.md/0.8.1.md
|
||||
и docs/MESSAGE-LATENCY.md/PERSONAL-MESSAGE-LATENCY.md. Не возвращайте expiry scan архива на каждый append.
|
||||
3. Для upload: docs/HTTP-UPLOAD.md, исторические VALIDATION-0.7.8.md/0.7.9.md.
|
||||
4. Для CI: docs/GITHUB-ACTIONS.md, docs/CI-VALIDATION.md,
|
||||
`.github/workflows/ci.yml`, scripts/ci/verify-all.sh.
|
||||
5. Выберите конкретный оставшийся пробел из COMPATIBILITY.md или текущей ошибки
|
||||
пользователя. Подтвердите его тестом до утверждения, что он исправлен.
|
||||
|
||||
## Карта исходников
|
||||
|
||||
- `src/server.rs`: runtime, TLS/SASL/bind, dispatch, доставка, запуск/остановка,
|
||||
MUC bootstrap для сохранённых SM-сессий.
|
||||
- `src/sessions.rs`, `src/session_store.rs`, `src/csi.rs`: XEP-0198, очереди,
|
||||
durable slots, fencing, resume, CSI.
|
||||
- `src/muc.rs`, `src/muc_recovery.rs`, `src/muc_history.rs`, `src/muc_admin.rs`:
|
||||
комнаты, журнал, восстановление, история и административные команды.
|
||||
- `src/storage.rs`: native Lua storage, атомарные записи.
|
||||
- Остальные подсистемы ищите по именам модулей; `rg` и `rg --files` предпочтительны.
|
||||
- `scripts/verify-local.sh`: основной набор регрессий.
|
||||
- `tests/*_network.py`: настоящие локальные TLS/HTTP/WS сценарии.
|
||||
- `docs/validation-logs-*`: зафиксированные результаты конкретных прошлых сборок.
|
||||
|
||||
## Инварианты, которые нельзя потерять
|
||||
|
||||
### MUC
|
||||
|
||||
`rust_muc_checkpoint` — авторитетный согласованный журнал Rust: конфигурация,
|
||||
affiliations, nick/роли/членство и ограниченная история, включая временные комнаты.
|
||||
Изменение фиксируется до ответа/публикации; ошибка записи откатывает состояние.
|
||||
Native config/state являются экспортом после commit. Устаревший native export
|
||||
не должен возвращать старые права, nick или историю после аварии.
|
||||
|
||||
Без действующей приватной SM-записи восстановление требует новой аутентификации
|
||||
точного полного JID и available presence либо явного MUC-входа. Текущие room ACL
|
||||
проверяются повторно. Уже ожидающий recovery deadline не продлевается restart,
|
||||
неудачной попыткой или восстановлением другого участника. Default grace300с;
|
||||
`muc_recovery_grace_period` ограничен1..604800.
|
||||
|
||||
С действующей durable SM-сессией логическое членство восстанавливается **до
|
||||
listeners**, после проверки аккаунта и room admission. Пока транспорт offline,
|
||||
room fanout должен попадать в сохранённую SM-очередь. При bootstrap не отправляйте
|
||||
синтетические join presence/history. Native MUC-файл сам по себе не является
|
||||
доказательством доверенной SM-сессии.
|
||||
|
||||
Критическая регрессия: Alice возобновила сессию и отправила сообщение, Bob ещё
|
||||
не аутентифицировался. Проверяйте сообщение в durable pending Bob ДО его входа.
|
||||
Поздняя история при восстановлении может ложно показать успешную доставку.
|
||||
См. tests/sm_restart_network.py.
|
||||
|
||||
### SM и доставка
|
||||
|
||||
SM snapshot приватный, atomic/fsync, с owner/token/fullJID/counters/pending/expiry.
|
||||
Проверяйте owner и токен; старые connection handles не могут менять новую сессию.
|
||||
Account deletion должна durably отозвать токены до purge. Повторно созданный
|
||||
аккаунт не должен принимать старый токен. Удаление собственного аккаунта всё ещё
|
||||
должно получить terminal IQ reply, несмотря на закрытую SM-сессию.
|
||||
|
||||
Handled увеличивается после обработки и только после SM enable. Подтверждения
|
||||
и изменения очереди сохраняются до соответствующей отправки. Offline expiry
|
||||
фиксирован; heartbeat обновляет только живой транспорт. Новый TLS/SASL обязателен:
|
||||
сохранения TCP/TLS fd между процессами нет. Гарантия replay — at least once,
|
||||
не exactly once между всеми хранилищами и сетью.
|
||||
|
||||
### Upload
|
||||
|
||||
Не разрушайте intent/exact-key multipart recovery, ownership isolation,
|
||||
completion quarantine, атомарное чтение свежего snapshot при захвате claim и
|
||||
source-preserving offline migration. HEAD404 после неопределённого Complete
|
||||
не доказывает отсутствие удалённого commit. Подробности — HTTP-UPLOAD.md.
|
||||
|
||||
## Проверки
|
||||
|
||||
Toolchain задан rust-toolchain.toml: Rust1.99.1, rustfmt, Clippy.
|
||||
Обычная проверка из корня:
|
||||
|
||||
```bash
|
||||
cargo fmt --check
|
||||
cargo test --locked
|
||||
cargo clippy --locked --all-targets -- -D warnings
|
||||
cargo build --release --locked
|
||||
bash scripts/verify-local.sh
|
||||
```
|
||||
|
||||
Изменения проверяйте подходящими тестами; не повторяйте дорогие неизменившиеся
|
||||
сценарии без причины. Для итогового расширенного прогона:
|
||||
|
||||
```bash
|
||||
XMPP_LOAD_CHECK=1 bash scripts/verify-local.sh
|
||||
```
|
||||
|
||||
Это ещё НЕ все ignored/large/native tests без дополнительных зависимостей.
|
||||
Полный CI entrypoint — `scripts/ci/verify-all.sh`; ему обязательны
|
||||
PROSODY_REFERENCE_SOURCE, PROSODY_REFERENCE_LUA и MINIO_BINARY. Он включает
|
||||
native oracle/federation, обе нагрузки500TLS, AF_UNIX admin, archive>128MiB,
|
||||
local и MinIO HTTPS PUT/GET10GiB. Нужны Lua5.4/native dependencies, OpenSSL и диск.
|
||||
Точные команды установки и подготовки — workflow и scripts/ci/prepare-*.sh.
|
||||
|
||||
Bundled оригинальная Prosody cafd74b9c5ea:
|
||||
`scripts/ci/vendor/prosody-cafd74b9c5ea.tar.gz`. Hash проверяется перед сборкой.
|
||||
Не заменяйте её иной версией для получения зелёного native oracle.
|
||||
|
||||
В текущей среде native зависимости извлекались в `/tmp/xmpp-native/usr`:
|
||||
|
||||
```bash
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
export CARGO_INCREMENTAL=0 CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0
|
||||
export LD_LIBRARY_PATH=/tmp/xmpp-native/usr/lib/x86_64-linux-gnu
|
||||
export LUA_PATH='/tmp/xmpp-native/usr/share/lua/5.4/?.lua;/tmp/xmpp-native/usr/share/lua/5.4/?/init.lua;;'
|
||||
export LUA_CPATH='/tmp/xmpp-native/usr/lib/x86_64-linux-gnu/lua/5.4/?.so;;'
|
||||
export PROSODY_NATIVE_PREFIX=/tmp/xmpp-native/usr
|
||||
export PROSODY_REFERENCE_SOURCE=/workspace/scratch/362577cdf2e0/reference/prosody-cafd74b9c5ea
|
||||
export PROSODY_REFERENCE_LUA=/tmp/xmpp-native/usr/bin/lua5.4
|
||||
```
|
||||
|
||||
Эти пути — только подсказка для сохранившейся среды. На обычном CI зависимости
|
||||
ставятся системно. Не используйте отсутствующий prefix как обязательное условие.
|
||||
При прежнем сбое linker `memory map must have a non-zero length` помогало
|
||||
`cargo clean -p rust-xmpp --release` перед повторной release-сборкой.
|
||||
Не удаляйте бинарник, используемый параллельным тестом: нагрузочный тест теперь
|
||||
копирует executable в отдельный каталог перед restart.
|
||||
|
||||
Полезные узкие проверки:
|
||||
|
||||
```bash
|
||||
python3 tests/muc_crash_network.py target/release/rust-xmpp
|
||||
python3 tests/sm_restart_network.py target/release/rust-xmpp
|
||||
python3 tests/muc_sm_load_network.py --binary target/release/rust-xmpp --clients 500
|
||||
```
|
||||
|
||||
Readiness timeout нагрузки увеличен до60с: восстановление500слотов заняло10.559с,
|
||||
поэтому старые5с давали ложный failure. Не скрывайте реальный сбой увеличением
|
||||
таймаута без диагностики. На перезапуске проверяйте доставку и counters, а не
|
||||
только открытие порта.
|
||||
|
||||
## Что действительно проверено
|
||||
|
||||
Rust0.8.0:293 tests passed,11 ignored в обычном cargo test; native дополнительно
|
||||
8module/storage+1federation. Все10 MUC crash cases прошли. Нагрузка:100аккаунтов,
|
||||
500TLS SM-ресурсов,10комнат,25000join presence,1500message deliveries, SIGKILL и
|
||||
resume10представителей. Результаты и ограничения — docs/VALIDATION-0.8.0.md.
|
||||
Сводный успешный лог составлен из завершённых частей и повторной нагрузки после
|
||||
исправления тестового readiness timeout; первоначальный failure сохранён отдельно.
|
||||
|
||||
10GiB local проверялся в0.7.8; MinIO в0.7.9 с раскрытой адаптацией EPERM при
|
||||
перечислении интерфейсов. Не приписывайте эти старые передачи новой сборке.
|
||||
CI собирает оригинальный MinIO без адаптации. AWS/distributed MinIO и настоящий
|
||||
APNs/FCM/production client cycle не подтверждены.
|
||||
|
||||
GitHub workflow проверен actionlint1.7.7, shell/Python syntax и случаями tags.
|
||||
Реального GitHub run, Docker build/run и Gitea push пока НЕ было: Docker daemon
|
||||
здесь отсутствует, доступ к пользовательскому GitHub/Gitea не предоставлен.
|
||||
|
||||
## Исправление задержки0.8.1
|
||||
|
||||
Полный MUC archive expiry на каждом сообщении был причиной seconds-level latency:
|
||||
100000records, median2518мс до →4.680мс после в realTLS loopback. Убраны no-op
|
||||
CSI checkpoints и перезаписи неизменённых native exports; private account traffic
|
||||
не ждёт MUC mutex. Обязательные durable commits сохранены. Четыре новые
|
||||
структурные регрессии прошли; измерения содержат raw samples и binary hashes.
|
||||
Результаты нового итогового прогона смотрите в docs/VALIDATION-0.8.1.md.
|
||||
Старые293tests и10GiB цифры выше относятся к прошлым выпускам.
|
||||
|
||||
## Личная доставка0.8.2
|
||||
|
||||
Не await push gateway в обработчике личного сообщения: очередь1024/max8worker,
|
||||
RAII cancellation при stop/error. Сначала durable offline/SM data, потомpush.
|
||||
Runtime CSI defaults теперь включают OMEMO encrypted/receipts/markers; explicit
|
||||
список заменяет defaults. Raw native importance function не менялась. Refresh
|
||||
policy при resume нужен для старых persisted slots. Логmessage-processing-slow
|
||||
не включает payload. Hub.for_owners выбирает account resources до session locks;
|
||||
не возвращайте сканирование всех чужих session locks при deliver/carbons.
|
||||
Archive.edges_store читает только endpoints для Bind2 и metadata IQ; сохранены
|
||||
bounded Lua/recovery/locks, middle не проверяется этим metadata operation.
|
||||
Новые тесты: personal_message_latency_network.py, push_latency_network.py,
|
||||
bind2_archive_latency_network.py, archive_edges.rs, session_routing_isolation.rs.
|
||||
Результаты текущей версии: docs/VALIDATION-0.8.2.md. Полный MAM query/maintenance
|
||||
и глобальный mutex SM Store остаются возможными отдельными IO bottlenecks.
|
||||
|
||||
## Ближайшие задачи и границы
|
||||
|
||||
- Проверить первый настоящий CI run после загрузки репозитория и настройки
|
||||
GITEA_REGISTRY/GITEA_IMAGE variables и USERNAME/TOKEN secrets по инструкции.
|
||||
Registry push разрешён основной ветке и version tags только после всех тестов.
|
||||
Образ должен содержать тот же executable, что проверенный binary artifact.
|
||||
- По новым сообщениям пользователя проверить реальные Conversations/Monocles,
|
||||
Monal/Gajim/Converse.js и целевой proxy/TLS. Старые жалобы PEP/DNSSEC не считайте
|
||||
закрытыми только потому, что локальный fixture зелёный. DNSSEC зоны требует
|
||||
отдельной проверки DNS/операторской конфигурации, не фиктивного server feature.
|
||||
- Оставшиеся продуктовые пробелы перечислены в COMPATIBILITY.md: один VirtualHost,
|
||||
часть федерации/CSI/PEP, нет transport rate limiting, произвольных Lua plugins,
|
||||
SQL и полного prosodyctl. Выбирайте и проверяйте конкретные задачи.
|
||||
- Для утверждения «полная замена» нужны реальные данные, внешняя федерация,
|
||||
настоящие клиенты/push, deployment/UID/TLS, rollback и длительная нагрузка.
|
||||
|
||||
После изменений обновляйте точные границы совместимости и результаты проверки.
|
||||
Не перезаписывайте исторические отчёты как будто они относятся к новой сборке.
|
||||
В передаваемый ZIP включайте AGENTS.md, workflow, vendor reference, исходники,
|
||||
документацию и scripts; исключайте target, demo data, bin, .git, **pycache**, pyc
|
||||
и секреты. Сохраняйте executable bits shell scripts и проверяйте содержимое ZIP.
|
||||
|
||||
## Durable message IO 0.8.3
|
||||
|
||||
Новый пользовательский log 0.8.2 содержит три local message-processing-slow
|
||||
4792–5549 мс. Таймер начинается внутри server после XML, но старые поля не
|
||||
доказывают причину IO. Контролируемый slow-sync fixture воспроизводит задержку
|
||||
1:1 в несколько секунд. MAM 4 sync/append и SM 2 sync/checkpoint заменены
|
||||
ограниченными checksummed redo journals. Обычная операция теперь требует 1 sync;
|
||||
создание, recovery и rotation дороже. Точные результаты — VALIDATION-0.8.3.md
|
||||
и DURABLE-MESSAGE-IO.md. Не приписывайте loopback причину production без phase logs.
|
||||
|
||||
SM `.wal` авторитетен, `.json` — readable export. Replay выполняется до JSON parse.
|
||||
Неполный trailing frame отбрасывается; неполный baseline или полная corruption
|
||||
блокируют startup. Durable tombstone предшествует delete. GC сначала делает
|
||||
sync каталога с JSON deletions, затем unlink WAL и sync каталога.
|
||||
Ordinary commit: append + sync_data, потом JSON export без sync. Ошибка export
|
||||
после commit не откатывает память; store блокирует дальнейшие записи до recovery.
|
||||
Precommit/sync error также блокирует store. Не возвращайте 2 sync на каждый
|
||||
checkpoint и не удаляйте redo без checkpoint. Общий SM mutex пока сохранён.
|
||||
Slot limit 8 MiB, total 256 MiB; WAL rotation 16 MiB плюс один slot, aggregate
|
||||
512 MiB. Не допускайте роста каждого из тысяч slots без aggregate cap.
|
||||
|
||||
Archive `.rredo`: native baseline и bounded anchor, checksummed record frames.
|
||||
Rotation после 128 frames/1 MiB плюс одна запись до 16 MiB. Cache ограничен 1024;
|
||||
изменение metadata вызывает перепроверку. Accepted records восстанавливаются
|
||||
даже при потере dirty native pages. Unjournaled native suffix не доказывает
|
||||
external append: truncate до committed end. Legacy `.rappend` ещё восстанавливается.
|
||||
Save/retain сначала durably фиксируют old native и удаление redo, затем
|
||||
делают atomic replacement. Смена inode при копировании/backup НЕ отменяет redo: journal
|
||||
остаётся авторитетным. Любой внешний writer, даже atomic replace, требует handoff. Purge включает rredo.
|
||||
Recovery повторно инвалидирует `.lidx`: unlink на hot path отдельно не dirsynced.
|
||||
Anchor проверяет только последние 4 KiB baseline, не всю старую историю.
|
||||
|
||||
Перед сменой writer/откатом остановите сервер и новым binary выполните
|
||||
`rust-xmpp storage-checkpoint CONFIG`. Внешний data-directory lock обязателен.
|
||||
`archive::checkpoint_all` и `session_store::checkpoint_exports` — только offline.
|
||||
Сначала sync native/current JSON и каталога, потом remove journal и sync каталога.
|
||||
Graceful stop сам по себе не означает handoff. Native Prosody не импортирует
|
||||
Rust SM sessions. Не редактируйте JSON export для expiry fixtures: используйте
|
||||
короткий настоящий timeout или проверяйте journal recovery. Не удаляйте WAL,
|
||||
чтобы получить зелёный тест. Native oracle перед Prosody modification явно
|
||||
вызывает checkpoint_all(root).
|
||||
|
||||
Новые tests: archive_redo.rs, sm_journal.rs, slow_storage_latency_network.py,
|
||||
slow_storage_crash_network.py, journal_recovery_sync_network.py, fixtures/slow_sync.c. Interposer действует только
|
||||
на изолированный fixture data_path. Обычный режим выполняет настоящий
|
||||
fsync/fdatasync; отдельный XMPP_SYNC_FAIL_SUFFIX вводит EIO только для fault test.
|
||||
Crash fixture проверяет journals и оба MAM архива до reauth после sender SM ACK
|
||||
и SIGKILL. Readable JSON без redo не подтверждает durability. Phase fields —
|
||||
server.rs и DURABLE-MESSAGE-IO.md.
|
||||
|
||||
В восстановленной среде native prefix теперь `/tmp/xmpp-native/usr/usr`, а не
|
||||
старый `/tmp/xmpp-native/usr`. `/tmp/xmpp083-env.sh` содержит env текущего прогона;
|
||||
это не portable config. Toolchain переустановлен; tests используют Rust 1.99.1.
|
||||
|
||||
Итог 0.8.3: 334 ordinary passed, 11 ignored; 9 native и 1 large-archive ignored
|
||||
выполнены отдельно. Все network checks и обе нагрузки 500 TLS прошли.
|
||||
Final binary SHA-256: 8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a.
|
||||
Подробные samples/границы — docs/VALIDATION-0.8.3.md. Checksum и JSON syntax всех frames
|
||||
проверяются при SM replay, но Record создаётся только для последнего
|
||||
snapshot. Recovery WAL sync до публикации обязателен; EIO fixture защищает это.
|
||||
|
||||
## Monal MAM baseline 0.8.4
|
||||
|
||||
Баг: «archive redo baseline changed in place» на живом сервере — внешняя
|
||||
перезапись native `.list` поверх зафиксированного redo-baseline (бакап/копия
|
||||
data_path/Prosody/старая версия) делала каждый `archive_for_user` Err, `?` ронял
|
||||
session, клиент (Monal-iOS) реконнектился и не доставлял сообщение. Причина не в
|
||||
обычном append — baseline ломает только перезапись первых `base` байт в обход
|
||||
`.rredo`; наш Rust-append пишет только в конец.
|
||||
|
||||
Фикс (commit 1000053): 1) MAM-архивация на личной доставке (отправитель,
|
||||
получатель, входящая федерация) стала best-effort `archive_message_best_effort` —
|
||||
storage-ошибка не роняет session и не роняет доставку, в лог `mam-archive-skipped`
|
||||
(санитизированный `sanitized_label`, без payload); 2) offline `storage-checkpoint`
|
||||
теперь ремонтирует непроверенный baseline: `prepare_rewrite` через
|
||||
`is_untrusted_redo` считает текущий синхронизированный native авторитетным и
|
||||
удаляет неприменимый журнал (`retire_redo`). Живой append остаётся fail-closed.
|
||||
Новые tests: `archive.rs` `runtime_append_fails_closed_...`/
|
||||
`offline_checkpoint_retires_...`, сетевой `tests/mam_broken_baseline_network.py`
|
||||
(перезапись baseline не роняет доставку; checkpoint возвращает MAM).
|
||||
|
||||
Критическое уточнение 0.8.4-followup: **живой append больше НЕ fail-closed** — это
|
||||
и было корневой причиной seconds-уровневых `message-processing-slow` у Luma и
|
||||
непрерывных `mam-archive-skipped` на сервере пользователя. В `src/archive.rs`
|
||||
`recover_redo` теперь сам зареТирует неприменимый журнал при первом же сбое
|
||||
(ранняя проверка anchor по первым 96 байтам header до чтения кадров) и продолжит
|
||||
работу на синхронизированном native как Prosody, который вообще не хранит `.rredo`.
|
||||
Чтение сломанного журнала больше не стоит O(журнал) на каждый access: проверяется
|
||||
только header, неподключённый журнал удаляется один раз, последующие access быстры
|
||||
(см. `untrusted_redo_baseline_is_retired_once_and_recovers`). Полная коррупция
|
||||
header-checksum или неполный baseline по-прежнему блокируют startup (инвариант
|
||||
durability 0.8.3) — они НЕ матчатся `is_untrusted_redo` и остаются fail-closed.
|
||||
|
||||
Сравнение с Prosody: `util/datamanager.lua` `list_append` пишет в конец, а
|
||||
`list_store` (перезапись) делает atomic rename — меняет первые bytes baseline;
|
||||
`plugins/mod_storage_internal.lua` `archive:append` для MUC/`key` и trim/delete
|
||||
делает `list_store` (полная перезапись). Prosody не понимает `.rredo` sidecar. Наш
|
||||
native `.list` сохраняет Prosody-совместимый `item(...)` формат.
|
||||
|
||||
Операционный ремонт пользователя: с этим фиксом ручной `storage-checkpoint` для
|
||||
починки рассинхронизированного baseline больше не требуется — журнал
|
||||
зареТиров согласуется automatically при первом доступе, и append продолжает
|
||||
работу на скорости Prosody. Offline `storage-checkpoint CONFIG` остаётся
|
||||
доступным (и идемпотентным) для штатного durable handoff перед сменой writer.
|
||||
`--experimental` не снимать. Полная
|
||||
замена Prosody требует реального CI, внешней федерации, настоящих клиентов/push,
|
||||
deployment/UID/TLS, rollback и длительной нагрузки — см. COMPATIBILITY.md.
|
||||
@@ -0,0 +1,39 @@
|
||||
# 0.8.0
|
||||
|
||||
- MUC сохраняет согласованный журнал конфигурации, affiliations, nick, ролей,
|
||||
членства и ограниченной истории без MAM. Изменение фиксируется до публикации
|
||||
ответа; при ошибке записи состояние комнаты откатывается.
|
||||
- Crash recovery использует последнее сохранённое поколение после SIGKILL,
|
||||
включая временные комнаты. Heartbeat обновляет живое членство;
|
||||
ожидающие записи сохраняют исходный deadline через повторные перезапуски.
|
||||
`muc_recovery_grace_period`: по умолчанию 300 секунд, пределы 1..604800.
|
||||
- Подтверждённые выходы, смена nick, ролей и политик комнаты не заменяются
|
||||
старой записью восстановления. Native config/state остаются экспортом для
|
||||
Prosody; согласованное восстановление Rust использует собственный sidecar.
|
||||
- Исправлены MUC probe, неизвестные типы presence, вход без MUC `<x/>`, ошибки
|
||||
доставки и статус 333. Обычные presence extensions сохраняются в живом
|
||||
обмене; поддельная MUC identity фильтруется. `subject` вместе с `body` либо
|
||||
`thread` не изменяет тему комнаты.
|
||||
- Подключены `muc_max_rooms`, `restrict_room_creation`, политики регистрации
|
||||
и зарегистрированного nick, `muc_tombstone_expiry`; добавлены встроенные
|
||||
административные ad-hoc команды уничтожения комнат и изменения affiliation.
|
||||
- Hub сохраняет resumable XEP-0198 сессии, счётчики и очередь. Resume после
|
||||
перезапуска доступен на новом TLS/SASL-транспорте через legacy SASL и SASL2.
|
||||
Действующие приватные SM-записи восстанавливают логические сессии и MUC
|
||||
до открытия listeners: offline-доставка попадает в очередь, синтетический
|
||||
join и история повторно не отправляются. SM resume не требует дополнительной
|
||||
available presence; без SM восстановление MUC требует новой аутентификации
|
||||
точного полного JID и available presence либо явного MUC-входа.
|
||||
Входящий handled-счётчик обновляется после обработки. Сокеты TCP/TLS
|
||||
не сохраняются; неопределённая сетевая отправка допускает повторную доставку.
|
||||
- Известные встроенные MUC-возможности Prosody отображаются на Rust-реализацию.
|
||||
Произвольные Lua/community-модули не исполняются; неизвестные явно включённые
|
||||
MUC-модули отклоняются при запуске. Общие и host `modules_disabled` учитываются
|
||||
для vCard и occupant-id.
|
||||
|
||||
Порядок восстановления, ограничения хранения и отката на Prosody:
|
||||
[docs/MUC-RECOVERY.md](docs/MUC-RECOVERY.md).
|
||||
|
||||
Проверка: 293 Rust tests, Clippy/fmt/release, полный сетевой прогон, 500 TLS
|
||||
MUC+SM ресурсов с SIGKILL/resume и native reference — успешно.
|
||||
Подробности: [docs/VALIDATION-0.8.0.md](docs/VALIDATION-0.8.0.md).
|
||||
@@ -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).
|
||||
@@ -0,0 +1,25 @@
|
||||
# 0.8.3 — durable IO личных сообщений
|
||||
|
||||
- Уменьшено число последовательных синхронизаций до доставки: ordinary MAM
|
||||
append использует один sync вместо четырёх, SM checkpoint — один вместо двух.
|
||||
Checksummed redo journals сохраняют commit-before-ACK и восстанавливают
|
||||
native archive/SM JSON exports после аварии.
|
||||
- Ограничены рост журналов, ротация и tombstone GC. Старый `.rappend` и legacy
|
||||
JSON импортируются. Retain/save/purge не возвращают удалённые записи.
|
||||
- Recovery синхронизирует валидный journal перед публикацией состояния; проверка
|
||||
checksum не заменяет durability barrier. EIO при adoption блокирует startup
|
||||
или offline handoff. Новые fault fixtures проверяют сохранность журнала и retry.
|
||||
- Replay SM проверяет checksum/JSON всех frames, но создаёт и валидирует полный
|
||||
Record только для последнего snapshot, сокращая работу с superseded queues.
|
||||
- `message-processing-slow` содержит времена MAM отправителя/получателя,
|
||||
доставки, carbons, offline/push и handled, а также число ресурсов/копий.
|
||||
Payload и секреты не добавляются в логи.
|
||||
- Добавлена offline команда `storage-checkpoint CONFIG`: durable экспорт и
|
||||
передача archive writer/подготовка отката на legacy Rust. Не запускайте другого
|
||||
writer и не удаляйте journals до успешного завершения команды.
|
||||
- Новые crash, partial-write, corruption, EIO, recovery, handoff и latency
|
||||
регрессии включены в GitHub Actions через verify-local.sh; отчёты сохраняются.
|
||||
|
||||
Измерения, ограничения и эксплуатация:
|
||||
[docs/DURABLE-MESSAGE-IO.md](docs/DURABLE-MESSAGE-IO.md),
|
||||
[docs/VALIDATION-0.8.3.md](docs/VALIDATION-0.8.3.md).
|
||||
Generated
+1
-1
@@ -1111,7 +1111,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-xmpp"
|
||||
version = "0.7.9"
|
||||
version = "0.8.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-xmpp"
|
||||
version = "0.7.9"
|
||||
version = "0.8.3"
|
||||
edition = "2024"
|
||||
description = "Experimental Rust XMPP server with Prosody-compatible storage"
|
||||
license = "MIT"
|
||||
|
||||
+12
-3
@@ -4,14 +4,23 @@ COPY Cargo.toml Cargo.lock ./
|
||||
COPY src ./src
|
||||
RUN cargo build --release --locked
|
||||
|
||||
FROM debian:trixie-slim
|
||||
FROM debian:trixie-slim AS runtime-base
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /src/target/release/rust-xmpp /usr/local/bin/rust-xmpp
|
||||
RUN ln -s /usr/local/bin/rust-xmpp /usr/local/bin/prosodyctl
|
||||
COPY scripts/docker-entrypoint.sh /usr/local/bin/rust-xmpp-entrypoint
|
||||
RUN chmod 0755 /usr/local/bin/rust-xmpp-entrypoint
|
||||
WORKDIR /work
|
||||
ENTRYPOINT ["rust-xmpp-entrypoint"]
|
||||
CMD ["serve", "/etc/prosody/prosody.cfg.lua", "--experimental"]
|
||||
|
||||
# CI downloads the exact executable produced by the verification job.
|
||||
FROM runtime-base AS ci-runtime
|
||||
COPY dist/rust-xmpp /usr/local/bin/rust-xmpp
|
||||
RUN chmod 0755 /usr/local/bin/rust-xmpp \
|
||||
&& ln -s /usr/local/bin/rust-xmpp /usr/local/bin/prosodyctl
|
||||
|
||||
# Default local build still compiles directly from source.
|
||||
FROM runtime-base AS runtime
|
||||
COPY --from=builder /src/target/release/rust-xmpp /usr/local/bin/rust-xmpp
|
||||
RUN ln -s /usr/local/bin/rust-xmpp /usr/local/bin/prosodyctl
|
||||
@@ -1,4 +1,4 @@
|
||||
# Rust XMPP 0.7.9
|
||||
# Rust XMPP 0.8.3
|
||||
|
||||
Сервер на Rust, разрабатываемый для замены **Prosody 13.0 nightly 100,
|
||||
cafd74b9c5ea** с предоставленными модулями. Читает существующий Lua-конфиг,
|
||||
@@ -16,7 +16,9 @@ PEP/pubsub, закладки, offline, carbons, SM, push, TLS-федерация
|
||||
Нестандартные HTTP-пути и TLS reverse proxy: [docs/HTTP-PROXY.md](docs/HTTP-PROXY.md).
|
||||
Пример для слияния с действующим конфигом: `examples/reverse-proxy.fragment.yaml`.
|
||||
|
||||
MUC-расширения и условия восстановления участников: [docs/MUC-RECOVERY.md](docs/MUC-RECOVERY.md).
|
||||
MUC и восстановление после сбоя, включая SM resume:
|
||||
[docs/MUC-RECOVERY.md](docs/MUC-RECOVERY.md).
|
||||
Изменения 0.8.0: [CHANGELOG-0.8.0.md](CHANGELOG-0.8.0.md).
|
||||
|
||||
Встроенный http_altconnect: [docs/HTTP-ALTCONNECT.md](docs/HTTP-ALTCONNECT.md).
|
||||
|
||||
@@ -150,7 +152,8 @@ inline SM/CSI/Carbons и MAM metadata. Добавьте его в modules_enable
|
||||
с `sasl2` только если эта функция нужна. Исходный конфиг по-прежнему работает
|
||||
без включения нового модуля. Ресурс генерируется случайно; прежние соединения
|
||||
того же аккаунта с тем же валидным UUIDv4 user-agent завершаются после успешного
|
||||
входа. Inline возобновление SM через SASL2 пока не объявляется.
|
||||
входа. Inline возобновление SM через SASL2 поддерживается, в том числе после
|
||||
перезапуска при наличии действующей сохранённой SM-сессии.
|
||||
|
||||
## PEP, push и pre-authenticated roster
|
||||
|
||||
@@ -206,3 +209,52 @@ gateway с multipart streaming описаны в [docs/HTTP-UPLOAD.md](docs/HTTP
|
||||
карантин неопределённого completion. Реальные 10 GiB через MinIO проверены с
|
||||
описанной адаптацией тестовой среды. Изменения и границы подтверждения —
|
||||
[CHANGELOG-0.7.9.md](CHANGELOG-0.7.9.md).
|
||||
|
||||
## MUC и SM после сбоя в 0.8.0
|
||||
|
||||
MUC фиксирует конфигурацию, роли, членство и ограниченную историю без MAM
|
||||
до публикации соответствующего ответа. Последний сохранённый журнал позволяет
|
||||
восстановить persistent и временные комнаты после аварийной остановки.
|
||||
Heartbeat обновляет живое состояние; срок уже ожидающего восстановления
|
||||
не продлевается повторным запуском. `muc_recovery_grace_period` задаётся
|
||||
в секундах: по умолчанию 300, пределы 1..604800.
|
||||
|
||||
Действующие приватные SM-записи восстанавливают логические сессии и MUC
|
||||
до открытия listeners, поэтому offline-доставка может попасть в сохранённую
|
||||
очередь. XEP-0198 resume через legacy SASL или SASL2 требует нового
|
||||
TLS/SASL-транспорта и токена того же владельца; TCP/TLS-сокеты не сохраняются.
|
||||
Без действующей SM-записи MUC ожидает новую аутентификацию точного полного JID
|
||||
и available presence либо обычный явный MUC-вход.
|
||||
|
||||
Native-файлы экспортируются для Prosody; источником согласованного crash
|
||||
recovery Rust служит собственный журнал. Перед откатом ознакомьтесь с
|
||||
[порядком переключения](docs/MUC-RECOVERY.md). Произвольные community Lua-модули
|
||||
не выполняются; неизвестные явно включённые MUC-модули отклоняются при запуске.
|
||||
Результаты проверки — в
|
||||
[docs/VALIDATION-0.8.0.md](docs/VALIDATION-0.8.0.md).
|
||||
|
||||
## GitHub Actions и Gitea Registry
|
||||
|
||||
Полные тесты, Linux binary artifact и Docker push после проверок:
|
||||
[настройка workflow](docs/GITHUB-ACTIONS.md). Registry и credentials задаются
|
||||
через GitHub Actions variables/secrets.
|
||||
|
||||
## Задержка сообщений в0.8.1
|
||||
|
||||
Устранены полный проход по MUC-архиву на каждом сообщении, повторные записи
|
||||
неизменённых native-файлов и no-op CSI checkpoints. Проверенный сценарий
|
||||
с100000 MAM-записей: median MUC delivery2.5с→4.7мс на loopback.
|
||||
[Причины, измерения и границы](docs/MESSAGE-LATENCY.md).
|
||||
|
||||
## Личные сообщения в0.8.2
|
||||
|
||||
Push routing больше не блокирует отправителя/SM ACK; CSI defaults немедленно
|
||||
передают bodyless OMEMO и receipts. Bind2/MAM metadata не сканируют всю историю.
|
||||
[Причины, тесты и конфигурация](docs/PERSONAL-MESSAGE-LATENCY.md).
|
||||
|
||||
## Durable IO личных сообщений в0.8.3
|
||||
|
||||
Сокращены последовательные sync MAM/SM через checksummed redo journals,
|
||||
добавлены замеры этапов медленной обработки и crash/replay проверки.
|
||||
Перед передачей файлов Prosody/откатом выполните offline `storage-checkpoint`.
|
||||
[Durability, команда handoff и диагностика](docs/DURABLE-MESSAGE-IO.md).
|
||||
@@ -0,0 +1,17 @@
|
||||
# Проверка изменения CI
|
||||
|
||||
2026-10-20. Изменяется только CI, упаковка Docker и документация; Rust source
|
||||
и версия0.8.0 не менялись. Предыдущие293 теста относятся к этому Rust source.
|
||||
|
||||
Локально успешно:
|
||||
|
||||
- actionlint1.7.7: `.github/workflows/ci.yml` без ошибок;
|
||||
- bash -n для всех трёх CI shell scripts;
|
||||
- Python compile и проверка генерации default-branch/version/SHA tags;
|
||||
- отклонение чужой ветки, несовпадающей версии, URL вместо hostname,
|
||||
некорректного image path и отсутствующего token;
|
||||
- SHA-256 bundled reference archive совпадает с ожидаемым.
|
||||
|
||||
Docker daemon отсутствует; GitHub Actions и реальный push в Gitea здесь
|
||||
не запускались. Результат следует проверить первым workflow run после
|
||||
добавления репозитория, variables и secrets по GITHUB-ACTIONS.md.
|
||||
+68
-19
@@ -2,26 +2,28 @@
|
||||
|
||||
Цель — конкретная Prosody `cafd74b9c5ea` и предоставленные community/custom
|
||||
модули ревизии `dddb6c34b964`, а не все существующие модули Prosody.
|
||||
Реализация содержит рабочие обработчики всех явно включённых имён модулей
|
||||
из конфига. Это не означает полного совпадения их поведения.
|
||||
Известные встроенные и выбранные модули предоставленной установки отображаются
|
||||
на обработчики Rust. Произвольные Lua/community-модули не исполняются;
|
||||
неизвестные явно включённые MUC-модули отклоняются при запуске.
|
||||
Это не означает полного совпадения поведения с Prosody.
|
||||
|
||||
| Область | Что реализовано | Существенные пределы |
|
||||
|---|---|---|
|
||||
| Конфигурация | Lua/JSON/YAML/YML/TOML, Include, глобальные и локальные области, наследование модулей | Lua VM ограничена; не все функции окружения конфигуратора Prosody доступны. Runtime обслуживает один VirtualHost и его компоненты |
|
||||
| Хранилище | internal `.dat`, `.list`, бинарные строки, смешанные ключи Lua, атомарные записи, потоковые архивы, инвалидирование `.lidx` | SQL, external storage остальных модулей и произвольные драйверы не реализованы; S3 uploads поддержаны. Каталог блокируется между процессами Rust; Prosody в блокировке не участвует |
|
||||
| Аккаунты, saslauth | internal_hashed SHA-1/SHA-256, SASLprep, PLAIN внутри TLS, SCRAM и PLUS с tls-exporter/tls-server-end-point, SSDP, SASL2 | Только internal_hashed; опциональный sasl2_bind2 (alias bind2) поддерживает inline bind/SM/CSI/Carbons. SASL2 inline SM resume и takeover живого транспорта реализованы; MAM metadata при Bind2 читает весь архив синхронно |
|
||||
| Сообщения и presence | локальная маршрутизация, приоритеты ресурсов, контакты/подписки/probes, offline, carbons, блокировки | Полная совместимость всех пограничных сценариев маршрутизации ещё не подтверждена |
|
||||
| MAM | личный и комнатный MAM2, prefs, формы, RSM, фильтры времени/контакта/ID, extended queries, metadata, stanza-id, expiry | Нет доказательства совпадения каждой ошибки и порядка результатов при всех комбинациях запроса |
|
||||
| MUC | комнаты, несколько ресурсов, роли/affiliations, owner/admin, конфигурация, приглашения, история без MAM и MAM, occupant-id/avatar, hats, язык, PM policies, presence broadcast, native metadata/state checkpoints и восстановление после штатной остановки | Восстановление после повторной аутентификации точного полного JID в300-секундном grace и при неизменной конфигурации. Нет сохранения TCP/SM между процессами и checkpoint на SIGKILL. Произвольные community-расширения не запускаются; MUC-RECOVERY.md |
|
||||
| PEP/pubsub | persistent nodes/items, ACL, subscriptions, publish/retract/purge/delete, owner/configuration, caps, OMEMO XML как payload | Не все необязательные политики узлов Prosody реализованы; OMEMO шифруют клиенты, криптография OMEMO не часть сервера |
|
||||
| private/bookmarks/vCard | private XML, старые и новые закладки, vCard/temp-vCard4 и avatar-переходы | Межклиентное поведение нужно дополнительно проверить настоящими клиентами |
|
||||
| SM/push | acknowledgements, resumable sessions, replay, ограниченные очереди, native регистрации push и ответы сервисов | Есть coalescing и watchdog первого push; полный цикл с настоящим APNs/FCM шлюзом не проверен. Состояние живых сессий не переживает перезапуск процесса |
|
||||
| CSI | active/inactive, ограниченный буфер, native importance rules, delay, flush при клиентской активности и grace period | Событие socket drain приближено завершением записи; необязательные расширенные XPath-фильтры поддержаны не полностью |
|
||||
| Федерация | входящие/исходящие 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 |
|
||||
| admin_shell | протокол admin socket, status/version/uptime, списки, основные операции аккаунтов | Нет произвольного Lua, горячей загрузки модулей и полного набора prosodyctl |
|
||||
| Остальные модули | disco, ping, time, version, uptime, register, TURN extdisco/HMAC, каталог seclabels | Нет встроенного TURN. Coturn работает отдельно. `pep_simple` пуст в предоставленном исходнике |
|
||||
| Область | Что реализовано | Существенные пределы |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Конфигурация | 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 живого транспорта реализованы; 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 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 |
|
||||
| admin_shell | протокол admin socket, status/version/uptime, списки, основные операции аккаунтов | Нет произвольного Lua, горячей загрузки модулей и полного набора prosodyctl |
|
||||
| Остальные модули | disco, ping, time, version, uptime, register, TURN extdisco/HMAC, каталог seclabels | Нет встроенного TURN. Coturn работает отдельно. `pep_simple` пуст в предоставленном исходнике |
|
||||
|
||||
## Особенности именно вашего конфига
|
||||
|
||||
@@ -141,3 +143,50 @@ HTTPS учитывает https_interfaces (включая* → IPv4+IPv6), HTTP
|
||||
PEP production причина по-прежнему не доказана; новая диагностика различает
|
||||
parser-valid IDs и форму уведомлений. Нет подтверждения Android UI, живого
|
||||
production WSS, DNSSEC signing или полной замены Prosody.
|
||||
|
||||
## Восстановление MUC и SM в 0.8.0
|
||||
|
||||
MUC-журнал хранит последнее согласованное поколение конфигурации, ролей,
|
||||
членства и ограниченной истории, включая временные комнаты без MAM.
|
||||
Неудачная фиксация откатывает изменение комнаты; внешний MAM-архив и прочие
|
||||
побочные эффекты не составляют единую транзакцию с журналом. Heartbeat обновляет
|
||||
живое состояние, но не продлевает deadline уже ожидающих записей.
|
||||
|
||||
При действующей сохранённой SM-сессии bootstrap восстанавливает логическую
|
||||
сессию и связанное MUC-членство до начала приёма соединений. Offline-доставка
|
||||
попадает в SM-очередь, а не в несуществующий сокет. Для сетевого resume нужны
|
||||
новая TLS/SASL-аутентификация и действующий токен владельца. Без такой SM-записи
|
||||
действует обычное повторное подтверждение полного JID и MUC admission.
|
||||
|
||||
Учтены `muc_max_rooms`, `restrict_room_creation`,
|
||||
`allow_unaffiliated_register`, `enforce_registered_nickname`,
|
||||
`muc_registration_include_form`, `muc_tombstone_expiry` и component-admin
|
||||
ad-hoc destroy/set-affiliation. Общие и host `modules_disabled` применяются
|
||||
к vCard и occupant-id. Эти встроенные обработчики не запускают Lua-плагины.
|
||||
Ограничения журнала, native-экспорта и отката:
|
||||
[MUC-RECOVERY.md](MUC-RECOVERY.md). Итоги проверки:
|
||||
[VALIDATION-0.8.0.md](VALIDATION-0.8.0.md).
|
||||
|
||||
## Durable IO0.8.3
|
||||
|
||||
MAM/SM используют authoritative redo sidecars. Перед сменой writer или откатом
|
||||
на старую версию требуется offline `storage-checkpoint CONFIG`; после аварии
|
||||
нельзя просто удалить sidecars. Подробности — [DURABLE-MESSAGE-IO.md](DURABLE-MESSAGE-IO.md).
|
||||
|
||||
### Внешний перезапись MAM baseline
|
||||
|
||||
Внешняя перезапись native `.list` поверх зафиксированного redo-baseline (бакап,
|
||||
копия data_path, Prosody, старая версия) давала «archive redo baseline changed
|
||||
in place»: каждый MAM-append падал и через `?` ронял session, клиент
|
||||
реконнектился, сообщение не доставлялось. В 0.8.4 MAM-архивация на личной
|
||||
доставке стала best-effort (storage-ошибка не роняет session и не роняет доставку;
|
||||
в лог — `mam-archive-skipped`), а `storage-checkpoint` получил ремонт непроверенного
|
||||
baseline: текущий синхронизированный native считается авторитетным и неприменимый
|
||||
журнал удаляется.
|
||||
|
||||
С 0.8.4-followup живой append больше НЕ fail-closed: `recover_redo` при первом же
|
||||
неприменимом baseline читает только header и сам удаляет журнал, принимая synced
|
||||
native (как Prosody), восстанавливая append на скорости Prosody без секундных
|
||||
`message-processing-slow` и прекращая `mam-archive-skipped`. Полная коррупция
|
||||
header-checksum или неполный baseline по-прежнему блокируют startup (fail-closed,
|
||||
инвариант 0.8.3) и не матчатся `is_untrusted_redo`.
|
||||
@@ -0,0 +1,143 @@
|
||||
# Личные сообщения и durable IO в 0.8.3
|
||||
|
||||
В присланном журнале сервера 0.8.2 есть три `message-processing-slow` с 4792–5549 мс,
|
||||
route=local. Этот таймер начинается после приёма XML и заканчивается после
|
||||
`mark_handled`: ожидание происходит внутри обработчика. Старые события не
|
||||
содержат разбивки по этапам и сами по себе не доказывают причину медленного IO.
|
||||
Исходный пользовательский журнал не включён в архив проекта.
|
||||
|
||||
## Изменение записи
|
||||
|
||||
До 0.8.3 append в MAM требовал 4 последовательных sync: rollback marker file+dir,
|
||||
native list, удаление marker+dir. Обычное сохранение SM требовало 2: JSON file+dir.
|
||||
В TLS fixture1:1 с двумя аккаунтами, четырьмя SM-ресурсами и carbons это обычно
|
||||
16 sync на сообщение. Увеличение задержки каждого настоящего fsync/fdatasync
|
||||
внутри изолированного data_path воспроизводит задержку в несколько секунд.
|
||||
Interposer не пропускает sync и не меняет системные настройки/production.
|
||||
|
||||
В 0.8.3 принятые данные сначала фиксируются в журнале восстановления с checksum:
|
||||
|
||||
- MAM `.rredo`: native `.list` сохраняет формат Prosody; обычный append требует
|
||||
одного sync журнала. Replay восстанавливает недостающий native suffix и
|
||||
отбрасывает незавершённый хвост. Полностью записанная повреждённая запись
|
||||
вызывает ошибку. Ротация —128 frames/1 MiB плюс максимум одна 16 MiB запись.
|
||||
- SM `.wal`: authoritative полные snapshots и tombstones; readable JSON —
|
||||
восстанавливаемый экспорт. Обычное сохранение требует одного sync_data.
|
||||
Ротация —16 MiB плюс максимум один snapshot; aggregate WAL cap 512 MiB,
|
||||
logical slots по-прежнему 256 MiB/8 MiB на slot/4096 stanzas и 4096 sessions.
|
||||
Tombstone GC сначала синхронизирует удаление JSON, затем удаляет WAL.
|
||||
|
||||
Recovery синхронизирует полностью проверенный журнал перед публикацией
|
||||
восстановленного состояния: полный frame после SIGKILL может остаться
|
||||
только в page cache. Checksum подтверждает целостность, но не запись на диск. Ошибка этого sync запрещает startup/handoff.
|
||||
|
||||
Создание журналов, восстановление и ротация дополнительно синхронизируют файлы
|
||||
и каталог. Экономия относится к обычной записи, не каждому возможному запросу.
|
||||
Отключения durability или периодической записи вместо commit-before-ACK нет.
|
||||
При неопределённой ошибке SM IO store блокирует дальнейшие изменения до
|
||||
восстановления. После успешного WAL commit ошибка экспорта не откатывает память.
|
||||
Queues/archives не образуют общей транзакции с сетью: гарантия replay остаётся
|
||||
at least once, а не exactly once между всеми хранилищами.
|
||||
|
||||
## Передача данных Prosody и откат версии
|
||||
|
||||
После SIGKILL/OS crash сначала используйте 0.8.3 для восстановления redo. Не
|
||||
удаляйте `.wal`/`.rredo` вручную и не редактируйте JSON export как источник истины.
|
||||
Для смены writer или отката на старую версию остановите сервер и выполните:
|
||||
|
||||
```bash
|
||||
rust-xmpp storage-checkpoint /etc/prosody/prosody.cfg.lua
|
||||
```
|
||||
|
||||
Команда берёт data-directory lock и SM store lock, восстанавливает журналы,
|
||||
синхронизирует native archives и актуальные JSON, затем durably удаляет redo.
|
||||
При ошибке передача владения не завершена; разберите ошибку, не запускайте
|
||||
другой writer. Перед работой с production данными сохраните обычную резервную
|
||||
копию всего data_path. Native Prosody не понимает resumable SM snapshots Rust;
|
||||
checkpoint SM обеспечивает совместимый JSON для старого Rust, не переносит
|
||||
TLS/TCP/SM-сессию в Prosody. Простой graceful stop не заменяет эту явную команду.
|
||||
Нельзя одновременно изменять `.list` средствами Prosody и Rust. Неjournaled
|
||||
in-place append при активном redo считается незавершённым и может быть отброшен.
|
||||
При восстановлении согласованной копии всего data_path журнал остаётся
|
||||
авторитетным независимо от смены inode файла. Любая внешняя модификация,
|
||||
включая atomic replacement, требует handoff. Внутренний rewrite сначала
|
||||
фиксирует старый native snapshot и удаление redo, затем атомарно заменяет native
|
||||
файл: авария в промежутке сохраняет старый snapshot, после замены — старый/новый.
|
||||
Редактирование baseline in place без handoff не поддержано. Anchor проверяет
|
||||
последние 4 KiB baseline, не является checksum всего старого архива.
|
||||
|
||||
## Внешний перезапись baseline: «archive redo baseline changed in place»
|
||||
|
||||
Если какой-либо writer (бакап-восстановление, копия data_path, Prosody, старая
|
||||
версия сервера) перезаписал native `.list` поверх зафиксированного baseline без
|
||||
handoff, `.rredo` перестаёт совпадать со своим anchor. Каждый вызов
|
||||
`archive_for_user` возвращал `Err`, и через `?` ошибка роняла session
|
||||
(`connection-closed reason=protocol-or-transport-error`) — клиент (напр. Monal)
|
||||
реконнектился по кругу, сообщение не доставлялось.
|
||||
|
||||
Начиная с 0.8.4 MAM-архивация на трёх местах личной доставки (отправитель,
|
||||
получатель, входящая федерация) стала best-effort: при storage-ошибке исходная
|
||||
stanza всё равно маршрутизируется, обе сессии остаются живы, в лог пишется
|
||||
санитизированный `mam-archive-skipped`.
|
||||
|
||||
Критическое уточнение (0.8.4-followup): живой append больше НЕ fail-closed.
|
||||
`recover_redo` при первом же untrusted-baseline сбое читает только 96-байтовый
|
||||
header (ранняя anchor-проверка до чтения/валидации кадров) и сам зареТирует
|
||||
неподключённый журнал (`retire_redo`), принимая синхронизированный native как
|
||||
Prosody. Это устраняет O(журнал) чтение+checksum на каждый access при больших
|
||||
сломанных журналах (секунды `message-processing-slow`) и возвращает append на
|
||||
скорость Prosody. Полная коррупция header-checksum или неполный baseline по-прежнему
|
||||
блокируют startup (инвариант 0.8.3): они не матчатся `is_untrusted_redo` и остаются
|
||||
fail-closed, чтобы не потерять committed redo frames без цели.
|
||||
|
||||
Ручной `storage-checkpoint` для этого больше не требуется; offline handoff
|
||||
остаётся идемпотентным способом штатного durable handoff перед сменой writer:
|
||||
|
||||
```bash
|
||||
rust-xmpp storage-checkpoint /etc/prosody/prosody.cfg.lua
|
||||
```
|
||||
|
||||
`prepare_rewrite` по-прежнему распознаёт непроверенный baseline (changed in place,
|
||||
shorter, missing, journal exceeds bound), считает текущий durably-синхронизированный
|
||||
native авторитетным, синхронизирует его и удаляет неприменимый журнал. После
|
||||
MAM history читается, а новые append создадут свежий redo поверх нового baseline.
|
||||
Сверку совместимости native-формата см. в исходниках Prosody
|
||||
(`util/datamanager.lua` `list_append`/`list_store`, `plugins/mod_storage_internal.lua`
|
||||
`archive:append`): Prosody пишет `item(...)` и при полной перезаписи применяет
|
||||
atomic rename — что и меняет первые bytes baseline, оставляя Rust-журнал устаревшим.
|
||||
|
||||
## Диагностика
|
||||
|
||||
`message-processing-slow` по-прежнему имеет threshold 250 мс, duration_ms, route
|
||||
и connection. Новые поля в integer milliseconds:
|
||||
|
||||
| Поле | Измеряемый этап |
|
||||
| ---------------- | -------------------------------------------------------------------- |
|
||||
| pre_routing_ms | Работа до личного MAM; для MUC/ранних выходов — обработка до handled |
|
||||
| sender_mam_ms | Политика и архивирование отправителя |
|
||||
| recipient_mam_ms | Проверка аккаунта, политика и архивирование получателя |
|
||||
| delivery_ms | Маршрутизация, targets, mutex и transmit |
|
||||
| carbons_ms | Выбор ресурсов и отправка carbon |
|
||||
| offline_push_ms | Offline commit и подготовка/enqueue push |
|
||||
| handled_ms | Session mutex и durable mark_handled |
|
||||
|
||||
Неисполненные этапы равны 0. target_count — выбранные локальные ресурсы,
|
||||
carbon_count — успешные carbon transmit. Это серверные этапы, не время
|
||||
отображения клиентом. Тексты сообщений, SASL и токены не логируются.
|
||||
|
||||
## Проверки и границы
|
||||
|
||||
`tests/slow_storage_latency_network.py` — реальный STARTTLS, MAM/carbons/PEP,
|
||||
контролируемый sync delay и счётчик операций. `tests/slow_storage_crash_network.py`
|
||||
— sender SM ACK, немедленный SIGKILL, проверка committed journals/обоих архивов
|
||||
до повторной аутентификации и replay всех ресурсов. Rust regressions проверяют
|
||||
partial tails, corruption, ротацию, tombstones, migration и offline handoff.
|
||||
Отдельный `journal_recovery_sync_network.py` вводит scoped EIO для WAL/redo
|
||||
и проверяет отказ startup/handoff без потери journal и успешный повтор.
|
||||
Точные измерения и результаты этой сборки — [VALIDATION.md](VALIDATION.md).
|
||||
|
||||
Общий mutex SM store, обязательное durable IO, большие pending snapshots,
|
||||
полные MAM queries/maintenance, PEP storage и federation всё ещё имеют стоимость.
|
||||
Медленный диск не становится быстрым от сокращения числа sync. Проверка на
|
||||
loopback не устанавливает причину каждого production warning; новые phase
|
||||
fields позволяют проверить это на целевой системе.
|
||||
@@ -0,0 +1,66 @@
|
||||
# GitHub Actions → private Gitea Registry
|
||||
|
||||
Workflow: `.github/workflows/ci.yml`. Push, pull request и ручной запуск выполняют
|
||||
проверки; публикация разрешена только для основной ветки репозитория и тегов
|
||||
`v<версия Cargo.toml>`. PR, включая PR из forks, не получает registry credentials.
|
||||
|
||||
## Настройка GitHub
|
||||
|
||||
Settings → Secrets and variables → Actions:
|
||||
|
||||
| Тип | Имя | Значение |
|
||||
|---|---|---|
|
||||
| Variable | `GITEA_REGISTRY` | `gitea.example.org` или `gitea.example.org:5000`, без `https://` и пути |
|
||||
| Variable | `GITEA_IMAGE` | `owner/rust-xmpp`, lowercase, без registry и тега |
|
||||
| Secret | `GITEA_REGISTRY_USERNAME` | Имя пользователя Gitea с правом публикации пакетов owner |
|
||||
| Secret | `GITEA_REGISTRY_TOKEN` | Personal access token с правом записи packages |
|
||||
|
||||
Создайте private owner/organization и проверьте права чтения пакета в Gitea:
|
||||
аутентифицированный push сам по себе не меняет публичность owner/package.
|
||||
Registry должен быть доступен GitHub-hosted runner по HTTPS с доверенным CA.
|
||||
Для registry только в LAN потребуется runner с доступом к этой сети; настройки
|
||||
runs-on для image job можно заменить на labels доверенного self-hosted runner.
|
||||
Не запускайте PR-код на привилегированном self-hosted runner без изоляции.
|
||||
|
||||
## Проверки и результат
|
||||
|
||||
Job `verify` на Ubuntu24.04 ставит Rust из `rust-toolchain.toml`, Python3.12,
|
||||
Lua5.4, OpenSSL/ICU и собирает оригинальную bundled Prosody cafd74b9c5ea.
|
||||
MinIO собирается из unmodified upstream RELEASE.2025-04-22T22-12-26Z,
|
||||
Go1.24.3, с проверкой SHA-256 исходного архива. Sandbox adapter не применяется.
|
||||
|
||||
`scripts/ci/verify-all.sh` запускает:
|
||||
|
||||
- fmt, все обычные Rust tests, Clippy и release build;
|
||||
- все сетевые проверки из verify-local.sh, native module/storage и federation;
|
||||
- обе нагрузки500TLS, в том числе MUC+SM SIGKILL/resume;
|
||||
- оставшиеся ignored tests: AF_UNIX admin socket и archive>128MiB;
|
||||
- реальные10GiB PUT/GET через MinIO и через local HTTPS backend, последовательно.
|
||||
|
||||
Таким образом отсутствие native reference или MinIO является ошибкой, а не skip.
|
||||
Нужно не менее20GiB свободного диска перед проверками. На одноразовом hosted runner
|
||||
удаляются неиспользуемые Android/.NET/GHC SDK; Go caches очищаются после сборки MinIO.
|
||||
Тесты могут занимать десятки минут. Timeout verify job —90минут.
|
||||
|
||||
Artifact `rust-xmpp-linux-amd64`: проверенный release executable и SHA256SUMS.
|
||||
Artifact `test-results`: полный лог и JSON-отчёты, также при ошибке теста.
|
||||
Job `image` скачивает именно этот executable, проверяет checksum и собирает
|
||||
Docker target `ci-runtime`. В образе выполняются `rust-xmpp --version` и
|
||||
`prosodyctl --version`, затем извлечённый executable сравнивается побайтно с artifact.
|
||||
Image smoke проходит и на PR. Только после успеха выполняются login/push.
|
||||
|
||||
Теги:
|
||||
|
||||
- `sha-<полный40символьныйcommit>` — каждый публикуемый commit;
|
||||
- `latest` — только основная ветка;
|
||||
- `v0.8.2` — пример version tag; он должен совпадать с Cargo.toml.
|
||||
|
||||
Version tag не меняет latest; prerelease также не перезаписывает latest.
|
||||
Собирается Linux amd64, не multiarch. Локальный `docker build .` по-прежнему
|
||||
собирает source target `runtime`; CI target использует уже проверенный binary.
|
||||
|
||||
Workflow добавлен и проверен локально как конфигурация. Реальный GitHub-run,
|
||||
Docker daemon и push в ваш Gitea здесь не выполнялись; для этого нужны репозиторий
|
||||
и перечисленные настройки. CI намеренно останавливает публикацию при любой ошибке.
|
||||
|
||||
Документация registry: https://docs.gitea.com/usage/packages/container/
|
||||
@@ -0,0 +1,63 @@
|
||||
# Задержка доставки: исправление 0.8.1
|
||||
|
||||
В0.8.0 MUC после каждого groupchat append запускал expire_before. Эта операция
|
||||
под общим mutex компонента проходила по всему архиву и записывала сохранённые
|
||||
записи во временный файл даже при отсутствии истёкших записей. Задержка каждого
|
||||
сообщения росла с размером истории. Дополнительно переписывались неизменённые
|
||||
native room config/persistent index, а CSI сохранял полную durable SM-сессию
|
||||
даже при пустой/неизменённой очереди.
|
||||
|
||||
В0.8.1 эти лишние операции исключены из обычного пути доставки. Очистка архива
|
||||
остаётся в периодическом maintenance (интервал сервера300с) и перед MAM-запросами.
|
||||
Неизменённые native-файлы не переписываются; недостающие/изменённые экспорты и
|
||||
повтор после ошибки сохранены. Авторитетный MUC checkpoint и SM state по-прежнему
|
||||
записываются до подтверждения соответствующих изменений. CSI откатывает изменение
|
||||
очереди при ошибке checkpoint. Ненаправленный в MUC account traffic обходит mutex
|
||||
компонента и не ждёт его обслуживания архивов.
|
||||
|
||||
## Измерение
|
||||
|
||||
Настоящий STARTTLS, два клиента, durable SM, MUC MAM с finite retention,
|
||||
active/inactive CSI. Изолированный loopback; архив native `.list` заполнен до
|
||||
запуска сервера. Проверяются доставка sender echo/peer, counters, durable CSI
|
||||
buffer/flush и точное число записей архива. Порог задержки в миллисекундах не
|
||||
используется как flaky assertion; структурные регрессии запрещают полный scan
|
||||
архива при append и запись неизменённых native-файлов.
|
||||
|
||||
Active CSI, MUC peer, мс:
|
||||
|
||||
| Записей истории | 0.8.0 median / p95 | 0.8.1 median / p95 |
|
||||
|---:|---:|---:|
|
||||
|0|9.425 / 11.221|5.054 / 7.643|
|
||||
|1000|32.718 / 36.995|4.280 / 6.605|
|
||||
|10000|259.472 / 269.067|4.895 / 6.307|
|
||||
|100000|2517.940 / 2520.941|4.680 / 14.532|
|
||||
|
||||
Inactive CSI,100000 записей: median3866.027→4.285мс,
|
||||
p95 4150.867→5.072мс. Первые три строки —5samples/state, последняя —3.
|
||||
Это ограниченное сравнение на одном loopback host, а не production SLA и не
|
||||
доказательство всех возможных причин задержки у реальных клиентов.
|
||||
Полные сырые samples и SHA-256 обеих сборок — validation-logs-0.8.1/latency-*.json.
|
||||
|
||||
Команды:
|
||||
|
||||
```bash
|
||||
python3 tests/message_latency_network.py --binary target/release/rust-xmpp \
|
||||
--rounds 5 --history-records 0 1000 10000 --output /tmp/message-latency.json
|
||||
python3 tests/message_latency_network.py --binary target/release/rust-xmpp \
|
||||
--rounds 3 --history-records 100000 --output /tmp/message-latency-100k.json
|
||||
```
|
||||
|
||||
Быстрая проверка0/10000 records уже включена в verify-local.sh и GitHub Actions.
|
||||
Для повторной медленной проверки используйте сохранённую0.8.0 release-сборку;
|
||||
не приписывайте исходному бинарнику результаты пересобранной новой версии.
|
||||
|
||||
## Если задержка остаётся в целевой установке
|
||||
|
||||
Сопоставьте время отправки, получения и отображения delivery/read receipt,
|
||||
тип чата, transport, состояние CSI и наличие федерации. Доставка сообщения и
|
||||
receipt — разные события. Native CSI policy для низкоприоритетных payload не
|
||||
менялась. Длительный MAM query, периодическая очистка большого архива, медленный
|
||||
диск или внешний XMPP-домен могут иметь отдельную задержку. Текущие изменения
|
||||
не отключают fsync, не обещают exactly-once и не подменяют проверку реальных
|
||||
Conversations/Monal/Gajim либо deployment.
|
||||
+132
-55
@@ -1,68 +1,145 @@
|
||||
# MUC: расширения и восстановление0.7.6
|
||||
# MUC: протокол и восстановление 0.8.0
|
||||
|
||||
Сопоставление выполнено с предоставленной Prosody cafd74b9c5ea: plugins/muc
|
||||
и mod_muc_mam.lua. Поддержка относится к встроенным расширениям этой ревизии,
|
||||
а не к произвольным community Lua plugins.
|
||||
Реализация сопоставлена с предоставленной Prosody `cafd74b9c5ea`, прежде всего
|
||||
`plugins/muc` и `mod_muc_mam.lua`. Известные встроенные возможности перенесены
|
||||
в Rust; произвольный Lua-код и community-модули Prosody не исполняются.
|
||||
Неизвестный явно включённый MUC-модуль отклоняется при запуске, а не молча
|
||||
игнорируется. Список `modules_disabled` применяется с учётом общих и host-настроек,
|
||||
в том числе для `muc_vcard` и `muc_occupant_id`.
|
||||
|
||||
Добавлены hats из affiliation data, язык комнаты, политики PM/модераторских PM,
|
||||
фильтрация presence по ролям и формы конфигурации. Поддельные occupant-id/hats
|
||||
и MUC user identity из клиентского сообщения удаляются. muc_vcard и
|
||||
muc_occupant_id можно отключить. Настройки MAM включают muc_log_all_rooms,
|
||||
muc_log_presences, muc_log_include_total; presence хранится отдельными событиями,
|
||||
обычный MAM-запрос выбирает groupchat сообщения как в native.
|
||||
## Что сохраняется
|
||||
|
||||
История работает без MAM: ограниченный буфер, maxstanzas/maxchars/seconds/since,
|
||||
delay и раздельный subject. Архив MAM продолжает поддерживать историю persistent
|
||||
комнат. Удаление комнаты очищает её MAM-архив.
|
||||
MUC теперь записывает единый журнал компонента `rust_muc_checkpoint`:
|
||||
конфигурацию комнат, affiliations, текущие nick и роли участников, безопасные
|
||||
поля presence и ограниченную историю обсуждения. Это относится к persistent
|
||||
и временным комнатам и работает без MAM. Журнал содержит согласованное поколение
|
||||
состояния; запись через временный файл, синхронизацию и rename предшествует
|
||||
возврату результата операции и публикации соответствующих MUC-ответов.
|
||||
При ошибке сохранения изменение состояния комнаты откатывается в памяти.
|
||||
|
||||
## Как восстанавливается членство
|
||||
Сохранение выполняется при изменениях членства, конфигурации, affiliations,
|
||||
ролей и истории, а также при штатной остановке. Поэтому восстановление больше
|
||||
не зависит только от SIGTERM: после SIGKILL используется последнее успешно
|
||||
зафиксированное поколение. Heartbeat обновляет отметки живых участников даже
|
||||
в тихой комнате. Неудачный heartbeat фиксируется в журнале сервера; состояние,
|
||||
которое не удалось сохранить, нельзя считать защищённым от следующего сбоя.
|
||||
|
||||
При SIGTERM/Ctrl-C сервер сначала сохраняет метаданные комнаты и native
|
||||
`state/<roomnode>.dat`, затем завершает соединения. Сохраняются nick, полный JID,
|
||||
роль, show/status/priority и последнее сообщение истории. Это относится также
|
||||
к временным комнатам с участниками. Транспортное отключение во время shutdown
|
||||
не считается добровольным выходом из комнаты и не удаляет checkpoint.
|
||||
Обычная история без MAM сохраняет порядок, время сообщений и ограничения
|
||||
`maxstanzas`, `maxchars`, `seconds`, `since`; subject хранится отдельно в
|
||||
конфигурации комнаты. MAM остаётся самостоятельным архивом и не превращается
|
||||
в часть одной транзакции со всеми внешними эффектами. Presence-события MAM
|
||||
и groupchat-запросы остаются раздельными.
|
||||
|
||||
После запуска сохранённые участники ожидают восстановления до300 секунд.
|
||||
Они не становятся активными адресатами маршрутизации автоматически. Когда
|
||||
тот же полный JID проходит SASL, bind и отправляет общую available presence,
|
||||
сервер повторно проверяет комнату и восстанавливает членство, nick, роль и
|
||||
presence. Одного совпадения bare JID или resource недостаточно.
|
||||
Клиент может по-прежнему явно войти обычной MUC presence.
|
||||
## Возвращение участников
|
||||
|
||||
Отпечаток конфигурации должен совпасть с сохранённым: изменение пароля или
|
||||
прав комнаты отменяет автоматическое восстановление; требуется обычный вход
|
||||
с актуальными параметрами. В presence state не записываются MUC-пароли и
|
||||
произвольные расширения. Пароль комнаты остаётся в исходной native config,
|
||||
как и до этого. Тот же checkpoint не продлевает grace при повторных перезапусках. Успешно использованная запись восстановления
|
||||
погашается на диске до отправки результата клиенту, чтобы авария не вернула
|
||||
старую роль после выхода или снятия модератора.
|
||||
Неактивированные временные комнаты очищаются после истечения срока.
|
||||
Без сохранённой SM-сессии после запуска членство становится ожидающей записью, а не активным
|
||||
адресатом маршрутизации. Автоматический MUC-вход возможен после новой
|
||||
аутентификации, bind того же полного JID и общей available presence. Bare JID
|
||||
или чужой resource не заменяют эту проверку. Обычный явный вход с MUC `<x/>`
|
||||
тоже доступен; актуальные ограничения комнаты проверяются повторно.
|
||||
|
||||
Native формат читается/пишется и проверяется настоящей Prosody в обе стороны.
|
||||
Для автоматического восстановления используются checkpoint с отпечатком и
|
||||
временем Rust; старый native state без этих полей сам по себе не является
|
||||
разрешением на автоматический вход. Обычное повторное присоединение доступно.
|
||||
Срок ожидания задаёт `muc_recovery_grace_period` в секундах: по умолчанию `300`,
|
||||
диапазон `1..604800` (значения ограничиваются этими пределами). Живое членство
|
||||
получает свежую отметку при фиксации и heartbeat. Уже ожидающая запись имеет
|
||||
фиксированный срок: запросы, неудачные попытки и повторные перезапуски его
|
||||
не продлевают. Неактивированные временные комнаты удаляются после истечения
|
||||
срока.
|
||||
|
||||
Это восстановление XMPP-членства в комнате: старые TCP/TLS-сокеты после
|
||||
остановки процесса не существуют. SM resume между процессами не реализован.
|
||||
Аварийный SIGKILL/отказ питания не создаёт новый checkpoint: гарантируется
|
||||
проверенный штатный shutdown, а не полный crash recovery.
|
||||
Смена пароля, affiliations или иной конфигурации отменяет старые ожидающие
|
||||
разрешения. Подтверждённые изменения роли, явный выход и удаление комнаты
|
||||
попадают в журнал до ответа, поэтому следующая авария не должна возвращать
|
||||
предыдущую роль или прежний nick. Успешное восстановление создаёт текущее
|
||||
членство: при следующей аварии восстанавливается его последняя сохранённая
|
||||
версия, а не навсегда погашенная старая запись.
|
||||
|
||||
История ограничена `max_history_messages`: по умолчанию100, допустимый внутренний
|
||||
предел4096. Состояние комнаты ограничено4096 ресурсами. Необязательные presence
|
||||
поля слишком большого размера и последнее сообщение больше16KiB не включаются
|
||||
в checkpoint; это не мешает сохранить членство. Текущие сообщения доставляются
|
||||
по обычным транспортным лимитам.
|
||||
В checkpoint presence входят только `show`, `status`, `priority`.
|
||||
MUC-пароли и произвольные расширения presence туда не копируются. При живом
|
||||
обмене обычные caps, avatar и idle сохраняются; присланные клиентом MUC identity,
|
||||
occupant-id и hats фильтруются. Пароль комнаты остаётся в её конфигурации.
|
||||
|
||||
## Проверка и границы
|
||||
Если имеется действующая приватная SM-запись Rust, логическая сессия продолжает
|
||||
членство без транспорта. При запуске повторно проверяются наличие и состояние
|
||||
аккаунта и текущие ограничения комнаты. Маршрутизация восстанавливается до
|
||||
приёма клиентов: сообщения для ещё не подключившегося участника попадают в его
|
||||
сохранённую SM-очередь. Синтетические join presence и история повторно не
|
||||
отправляются. Доставка по сети требует новой TLS/SASL-аутентификации и корректного
|
||||
SM-токена того же владельца; native MUC-файлы сами по себе этого не разрешают.
|
||||
|
||||
Автоматические тесты используют отдельные временные данные. Сетевой сценарий
|
||||
останавливает процесс с живыми клиентскими socket, запускает его повторно,
|
||||
проверяет persistent и temporary комнаты, запрет чужих identity/resource,
|
||||
восстановленные роли/presence и доставку сообщений. Native oracle проверяет
|
||||
перенос state с несколькими ресурсами туда и обратно.
|
||||
## Native-файлы и переход между серверами
|
||||
|
||||
Полная production-миграция, непрерывные федеративные reconnect и все community
|
||||
расширения не проверены. Конкретные результаты — VALIDATION.md; общие пределы
|
||||
сервера — COMPATIBILITY.md. Реальные клиенты должны повторно аутентифицироваться.
|
||||
Native config, persistent index и `state/<roomnode>.dat` читаются и экспортируются
|
||||
для совместимости с Prosody. При работе Rust источником согласованного
|
||||
восстановления служит его журнал, а native-файлы являются экспортом после commit.
|
||||
Устаревший экспорт после сбоя не должен возвращать предыдущее членство.
|
||||
Native state без подтверждающих Rust-метаданных сам по себе не разрешает
|
||||
автоматический вход.
|
||||
|
||||
Редактирование native-конфигурации администратором обнаруживается по отпечатку:
|
||||
старые разрешения восстановления и история, привязанная к прежней политике,
|
||||
отбрасываются; удаление ранее экспортированной конфигурации отменяет её
|
||||
восстановление. Это не универсальное слияние параллельных записей двух серверов.
|
||||
|
||||
Для отката на Prosody остановите Rust и сохраните резервную копию всего
|
||||
`data_path`. Начиная с0.8.3 перед сменой writer выполните новым binary
|
||||
`rust-xmpp storage-checkpoint CONFIG`: он фиксирует MAM redo и SM JSON exports.
|
||||
Это не перенос Rust SM-сессий в Prosody и не универсальная конверсия MUC journal.
|
||||
Подробности — [DURABLE-MESSAGE-IO.md](DURABLE-MESSAGE-IO.md). Prosody использует native-экспорт и не читает Rust-журнал. Перед
|
||||
возвратом с Prosody на Rust согласуйте данные и устраните устаревший
|
||||
`rust_muc_checkpoint` в остановленном экземпляре: иначе неизменившиеся native
|
||||
файлы могут быть приняты за экспорт предыдущего поколения Rust. Удаление
|
||||
журнала означает отказ от его crash recovery, включая историю временных комнат.
|
||||
Не запускайте оба сервера с общей записываемой директорией.
|
||||
|
||||
## Поддерживаемые политики
|
||||
|
||||
- Доступны hats из affiliation data, язык комнаты, роли presence broadcast,
|
||||
политики PM и модераторских PM, формы конфигурации и offline affiliation presence.
|
||||
- `muc_vcard` и `muc_occupant_id` управляют соответствующими возможностями.
|
||||
MAM учитывает `muc_log_all_rooms`, `muc_log_presences`, `muc_log_include_total`.
|
||||
- `restrict_room_creation`: `false` разрешает создание по умолчанию, `true`
|
||||
ограничивает его настроенными администраторами, `"local"` — локальными
|
||||
зарегистрированными пользователями и администраторами. `muc_max_rooms`
|
||||
ограничивает число комнат; существующее членство не требует повторного
|
||||
разрешения на создание.
|
||||
- Регистрация учитывает `allow_unaffiliated_register`,
|
||||
`enforce_registered_nickname`, `muc_registration_include_form`.
|
||||
Срок tombstone задаёт `muc_tombstone_expiry` (по умолчанию 31 день).
|
||||
- Администраторам компонента доступны ad-hoc команды
|
||||
`http://prosody.im/protocol/muc#destroy` и
|
||||
`http://prosody.im/protocol/muc#set-affiliation`. Полномочия проверяются
|
||||
на каждом шаге; изменения проходят обычный механизм фиксации MUC.
|
||||
|
||||
Presence probe не создаёт участника и возвращает сведения только допущенному
|
||||
участнику. Новый вход без MUC `<x/>` отклоняется; неизвестные типы presence
|
||||
не превращаются во вход. Presence errors исключают соответствующую сессию;
|
||||
message errors делают это для native-набора ошибок доставки. Последняя сессия
|
||||
получает статус `333`; остальные ошибки приватного сообщения могут быть
|
||||
переданы адресату. Сообщение с `subject` и `body` либо `thread` остаётся обычным
|
||||
сообщением и не меняет тему комнаты.
|
||||
|
||||
## Границы и проверка
|
||||
|
||||
Старые TCP/TLS-сокеты после остановки процесса не существуют. Восстановление
|
||||
членства MUC и XEP-0198 resume — отдельные механизмы. В 0.8.0 Hub сохраняет resumable
|
||||
SM-сессии, счётчики и очередь неподтверждённых stanza. Resume проходит на новом
|
||||
защищённом аутентифицированном транспорте, включая legacy SASL и SASL2;
|
||||
успешный SM resume восстанавливает MUC без дополнительной available presence.
|
||||
Входящая stanza учитывается в handled-счётчике после обработки. Heartbeat и
|
||||
штатная остановка сохраняют снимок; offline deadline не продлевается простыми
|
||||
проверками или повторными запусками.
|
||||
|
||||
SM-снимок приватный: предел 256 MiB суммарно, 8 MiB и 4096 stanza на сессию.
|
||||
Он не является native-форматом Prosody. Нельзя обещать сохранение сокета или
|
||||
ровно одну доставку при неопределённом результате сетевой отправки.
|
||||
|
||||
Буфер истории ограничен `max_history_messages` (по умолчанию 100, предел 4096),
|
||||
состояние комнаты — 4096 ресурсами. Большие необязательные поля presence
|
||||
исключаются из checkpoint; отдельные сообщения истории больше 16 KiB не
|
||||
восстанавливаются. Общий checkpoint компонента ограничен 16 MiB: превышение
|
||||
лимита приводит к ошибке записи, а не к успешному подтверждению несохранённого
|
||||
изменения. Долговечность предполагает исправный диск и файловую систему,
|
||||
соблюдающую синхронизацию записи.
|
||||
|
||||
Команды и результаты проверки версии 0.8.0 публикуются в
|
||||
[VALIDATION-0.8.0.md](VALIDATION-0.8.0.md). Полный прогон завершён успешно. Общие границы совместимости:
|
||||
[COMPATIBILITY.md](COMPATIBILITY.md).
|
||||
@@ -0,0 +1,107 @@
|
||||
# Личные сообщения: исправления0.8.2
|
||||
|
||||
Предыдущий0.8.1 устранил per-message MUC archive scan и часть лишних CSI writes.
|
||||
Отдельная проверка личных чатов обнаружила другие причины. Испытания ниже
|
||||
проведены на loopback; это не подтверждение поведения конкретной production-сессии.
|
||||
|
||||
## Push не должен останавливать чат
|
||||
|
||||
В прежнем обработчике после локальной доставки выполнялся await внешнего
|
||||
push IQ route и только потом обновлялся handled и читалось следующее сообщение.
|
||||
Неполадки DNS/TCP/TLS/SASL внешнего шлюза задерживали отправителя до сетевого
|
||||
таймаута (30с на route). При полностью offline получателе push даже отправлялся
|
||||
раньше записи offline archive.
|
||||
|
||||
Регрессия воспроизвела настоящий исходящий STARTTLS к шлюзу, который после TLS
|
||||
не присылает features. У0.8.1 через2с первое сообщение уже в durable SM pending,
|
||||
второго нет, SM ACK отсутствует. У0.8.0 с полностью offline получателем в этот
|
||||
момент ещё нет даже первого offline record. В0.8.2 в обоих сценариях оба сообщения
|
||||
сохранены и sender ACK h=4 соответствует входящим4 stanza при том же зависшем
|
||||
шлюзе. Offline records присутствуют уже при первом post-TLS stream gateway.
|
||||
|
||||
Уведомления теперь маршрутизирует ограниченный worker:1024queued,8in-flight.
|
||||
При переполнении/ошибке notification routing пишет warning; подтверждение
|
||||
сообщения не означает успешный push. Очередь уведомлений не durable и при
|
||||
остановке отменяется; сами сообщения сохраняются независимо. Проверен XMPP
|
||||
push fixture, не APNs/FCM и не промышленный мобильный цикл.
|
||||
|
||||
## CSI и зашифрованные сообщения без body
|
||||
|
||||
Прежняя policy считала unimportant receipt-only и ciphertext-only OMEMO без
|
||||
body/EME. При `inactive` они могли ждать активности клиента. В тесте намеренный
|
||||
dwell2с показан отдельно: это время удержания, не вычислительная задержка.
|
||||
Сервер уже быстро отвечал SM ACK, но пользователь ещё не видел payload/receipt.
|
||||
|
||||
Default `csi_important_payloads` в0.8.2:
|
||||
|
||||
- `{eu.siacs.conversations.axolotl}encrypted`
|
||||
- `{urn:xmpp:omemo:2}encrypted`
|
||||
- `{urn:xmpp:receipts}received`
|
||||
- `{urn:xmpp:chat-markers:0}received`
|
||||
- `{urn:xmpp:chat-markers:0}displayed`
|
||||
- `{urn:xmpp:chat-markers:0}acknowledged`
|
||||
|
||||
Они выходят без ожидания active. Body, EME и остальные прежние important payload
|
||||
сохраняют поведение; headline и прочие unimportant данные продолжают буферизоваться.
|
||||
Явный список заменяет defaults: если уже настроили свой список, добавьте нужные
|
||||
пути туда. Для строгой прежней policy в JSON используйте `csi_important_payloads: []`;
|
||||
эквивалентный явно непустой Lua-list без новых extensions:
|
||||
`csi_important_payloads = { "{urn:xmpp:eme:0}encryption" }` (EME уже important).
|
||||
Defaults увеличивают число немедленных доставок и возможных пробуждений клиента.
|
||||
Старая importance function/native oracle сохранена; изменён именно runtime default.
|
||||
На bind и resume применяется текущая конфигурация, включая сохранённые старые slots.
|
||||
|
||||
В6TLS fixtures:100000MAM/user, roster0/5000/10000,500durable pending, default/explicit[]:
|
||||
все проверки прошли. Default inactive OMEMO legacy/2 —2.58–4.06мс,
|
||||
receipts —7.36–35.56мс; при explicit[] сохраняется hold до active.
|
||||
Headline buffering, EME delivery и точный счёт100523MAM records/user проверены.
|
||||
Это XML/wire проверка доставки ciphertext, не проверка OMEMO криптографии клиента.
|
||||
|
||||
## Большой личный архив при входе
|
||||
|
||||
Bind2 и обычный MAM metadata IQ раньше декодировали все записи ради start/end.
|
||||
На100000records,5samples:
|
||||
|
||||
| Операция | Старая медиана |0.8.2медиана |
|
||||
|---|---:|---:|
|
||||
|SASL2 Bind2 authenticate→success|2742.353мс|1.976мс|
|
||||
|TCP/STARTTLS→Bind2 success|2745.115мс|3.565мс|
|
||||
|MAM metadata IQ|2437.945мс|0.355мс|
|
||||
|
||||
Baseline0.8.0 содержит тот же прежний scan path; hash сохранён в raw report.
|
||||
Новый edges_store читает ограниченные endpoint records под существующей блокировкой,
|
||||
не создаёт/не требует index и восстанавливает partial append. Проверяются exact
|
||||
IDs/timestamps. Это операция metadata, не проверка integrity всех промежуточных
|
||||
записей. Полные MAM queries и периодическая очистка всё ещё могут читать архив.
|
||||
|
||||
## Проверка и диагностика
|
||||
|
||||
Новые scripts:
|
||||
|
||||
```bash
|
||||
python3 tests/push_latency_network.py --binary target/release/rust-xmpp --recipient hibernating
|
||||
python3 tests/push_latency_network.py --binary target/release/rust-xmpp --recipient offline
|
||||
python3 tests/personal_message_latency_network.py --binary target/release/rust-xmpp
|
||||
python3 tests/bind2_archive_latency_network.py --binary target/release/rust-xmpp --records 100000 --samples 5
|
||||
```
|
||||
|
||||
Быстрые варианты включены в verify-local.sh и CI. Push fixture использует
|
||||
loopback port5269; конфликт порта является ошибкой fixture. Global DNS/resolv.conf
|
||||
не меняются; fixture использует special-use localhost и другой account domain.
|
||||
|
||||
`message-processing-slow` warning содержит duration_ms, route и correlation connection.
|
||||
Оно фиксирует завершившуюся серверную обработку>250мс, без текста сообщения.
|
||||
Если задержка остаётся, сопоставьте его с клиентским временем, типом transport,
|
||||
CSI state, регистрацией push и внешней федерацией. Отсутствие warning не доказывает
|
||||
отсутствие сетевого/клиентского ожидания до/после обработки.
|
||||
|
||||
SM Store пока использует общий mutex для durable slot IO; обязательный fsync
|
||||
не отключён. Полные архивные запросы/maintenance, большие roster, медленный диск
|
||||
и внешняя федерация имеют собственную стоимость. Производственная квалификация
|
||||
клиентов и полная замена Prosody остаются за рамками этих fixtures.
|
||||
Итоговые тесты/сырые отчёты: VALIDATION-0.8.2.md и validation-logs-0.8.2/.
|
||||
|
||||
## Продолжение0.8.3
|
||||
|
||||
Новые логи показали seconds-level серверную обработку. Добавлены redo MAM/SM
|
||||
и разбивка времени на этапы: [DURABLE-MESSAGE-IO.md](DURABLE-MESSAGE-IO.md).
|
||||
+57
-23
@@ -1,8 +1,20 @@
|
||||
9984392a77fcbb6d0ee70b536ecdcfc4349101bb300a509e32353aaa1134979e .github/workflows/ci.yml
|
||||
6593266e57a718762fbca26fd893fcabf1ea842dcc53dd0fd11844b2def2a543 AGENTS.md
|
||||
b83b20e991af384b349825ca39aad157d8b412d0143cc34c68567ca011bccf58 Cargo.lock
|
||||
3ecc46532a7fb1a231dbbd5337221d0c0c32ba36574e74655854f1a1d54d4581 Cargo.toml
|
||||
6be2fbcd1da8a07dda7daf3ddd727b38f363751ebace9c0b87c282f8961cffed Dockerfile
|
||||
7136ea911bb6b6d352f703f80efeebde921028ea9dc39cd0d2cd55ea465f8c37 examples/config.json
|
||||
8a65739eb5c24f10b9f2c509f0bd050fb1115706e6a41fdf804b9e0e44f583fd examples/config.toml
|
||||
ca86274bcf4dc5daebbf69c9f003854afca92954719aff068f810e0a3d615dea examples/config.yaml
|
||||
bbc2591581d382fef5fb918a480e6c338c7e3e3f2025df5509f51708534631e7 examples/converse-origin.fragment.yaml
|
||||
66ea9e2b58f6a39b70911b9981cae5e99625483af7a41168e24923464de8ae56 examples/reverse-proxy.fragment.yaml
|
||||
d13306be0ecda81ce779baa290ffb417380d525928477f16200884b5169cca33 rust-toolchain.toml
|
||||
c08bd5923906f4d2163a258c8b68dbef39ea6a3e07032f67e5c12d3d9ca81c5e scripts/ci/image-tags.py
|
||||
8820d262f8018a822b27eb9e5f0d3b9ca0c1272001164ca8a59fcd13911a5426 scripts/ci/prepare-minio.sh
|
||||
5bcd646c6c9159bbdce3a4eadce4d80d6c7422748e47c6ad04e5769efb4f2920 scripts/ci/prepare-reference.sh
|
||||
cb44f2692aefd618872153aed2658a7ab5c1a420802592e3240553b1f8da5ea5 scripts/ci/vendor/README.md
|
||||
1e558d3d2211ea0228596aced2b71306503190c454ec4cb44f172045b3513c34 scripts/ci/vendor/prosody-cafd74b9c5ea.tar.gz
|
||||
7ae11639143308ed4acd30ef6f99f55c7575893e5f6dce819d1f9d7da00cc29c scripts/ci/verify-all.sh
|
||||
16e476e50c5802efcbb7d1aa68141c183c60f5ab17037d697dce720c209141f4 scripts/docker-entrypoint.sh
|
||||
6fcb8a4977fee840d826951b0c30a3f2477c55d00860c79e99f635ca6e215e2b scripts/filter-android-pep-log.py
|
||||
a83cfdb6d910af1add9a14df7b0bd43fb0e2a23342f1a1eec831d47c659bba6f scripts/integration_http.py
|
||||
@@ -14,13 +26,13 @@ b1018e1b279c89f6cf14b4017e4f1b14e9165afef2f668cec6ac8bbe0c710d85 scripts/integr
|
||||
e08ac6136cce27cda2ddbd510da0f25750766ab0150a9f4b8f87c953276d16e9 scripts/prosody_compat/oracle.lua
|
||||
2c7a2c946885205e9df38b97305073a8ac72f58ac8ff84fc6f9af320c35bbc96 scripts/prosody_compat/verify.sh
|
||||
1b9ed063daf3e0320819b4fe0135c96dd27e49877044aa2f5093b8ab4fbda41d scripts/smoke.py
|
||||
0b2f6209a36fe9783b83d43396da26862c89cad1da1483ae49a79800e31982c1 scripts/verify-local.sh
|
||||
2cd5e17cdf0902ceaaca9b6e8d36ca766e33e66a06784ec97ad99165571eaf91 scripts/verify-local.sh
|
||||
a2e47f547ab9349f84ee6c4f8e51c147b7110b41a82c17cdb938027a84d82ef3 src/account_modules.rs
|
||||
5637f816586f0613b83e14d0ed478bd539f325b70ccbfc9fdf893adbe494453d src/admin.rs
|
||||
89d7e29d8f8daab614c9c836864b166fdaa324ca035c2d3d5f95639c24b2a9e1 src/archive.rs
|
||||
1b2011fb38482eba69dc245c367ddf496089d0cbde8a000b1ff5e9bbf8c3dea8 src/archive.rs
|
||||
e15b079a420227ee59e3b7ac305783c51eb59d76a8fb5dd12760547b0f812107 src/auth.rs
|
||||
83ac35a4c9d20debf006fe80fba8e423c09806fb2526ff384b188ca28d612a5e src/config.rs
|
||||
6574ad70d5c60c4acf7e01e66dadaa7cd2d09d3b184a611418aeb271f27ebc3c src/csi.rs
|
||||
175ab07f17ba14c51c76857c4905b0a587d8be68e2be5179456e9c71a523c487 src/csi.rs
|
||||
834ebe4a1f2d46ab932c14c0a2c7be3375873a71ef56026d372c86e60ef041fc src/data_lock.rs
|
||||
1c8aa5cb816a32a0f9f24589ac00beba27544a83c3d9aad771523a1d1cbcd4c8 src/extras.rs
|
||||
dca8959effb123b94e1fbd42c561a79c721178c4b612a004db9f9fccb3b8950d src/federation.rs
|
||||
@@ -29,32 +41,37 @@ d561a6bd4db079cfb1ef3bb7ae75f23e6080c5d5705583774f6c5f1ca6ed4420 src/http_confi
|
||||
3fa73ae129fe552bd7de7afca0617a1c18bf4d3b36a39023bfec036bca17ffc5 src/http_proxy.rs
|
||||
f4f87f5c20cb0602d803e86a22c209756c3b1f22c532743ec7e0dbf5bb174a0e src/http_service.rs
|
||||
c604a0a764ffac6bae0c9aa15c9ef7022d35cb1f8c9159eb66257114445621c5 src/http_transport.rs
|
||||
c5b7bcc0d08e48fd3805d6571428bb83b7fdc6f6e23a65bf689f079ad87bc32a src/lib.rs
|
||||
5b27b6c719537af919bd6fe089574eba5e2c7bbe0d32249164914a4bb57a1427 src/lib.rs
|
||||
a21e1119ea0271b63935e92eb106ab6076f0035e6d3a06c21a352f152b9b37db src/logging.rs
|
||||
58ce2a9daa36ac26b39c8f263a16f4fa8cc4e57c91bbbf5b24a6aad3f1a326e0 src/lua.rs
|
||||
a264e2765a31c8c096462551aca229530cad356e39e338370d1a0bdd97860680 src/main.rs
|
||||
71d5d185900b9cde190e10e2f28371cd5042d98b5900b4385d823f724234fced src/mam.rs
|
||||
0f780d95523b41c4ae242bd4a1fb6b65ff57826dca6d23ac73a1a1459609ec0a src/muc.rs
|
||||
d8263eb28f5cdff362512a6b13217aa8f7574f4a3d54b7bcb6ba7930f8c353d8 src/muc_history.rs
|
||||
4c151ffcdc36e4a3e680f859d4dc7ba16b0162d4f1fe6fb470df4c5e21197e5a src/muc_recovery.rs
|
||||
f103c15e11d61b5818e9a9491b8533b904cade7b3654c481cc61d3a7d0bb8173 src/main.rs
|
||||
68d97d96aa3a370474c144ccfdbf16109c46a6b14123d55d30464237ae29a958 src/mam.rs
|
||||
f17b06fe5b76381b5cdbfc2b668107f6c2b0b456c05b7a7709bea6a284531ff5 src/muc.rs
|
||||
c7ba46a140b9dce65cc573176265d6f77049ae657a70ffe59611a1c0f3ef89af src/muc_admin.rs
|
||||
ed2f07105ffeb80396aaf91c7a1fe9a8dc88d17235d03a0796372c1797c1506a src/muc_history.rs
|
||||
636f6ea7ad7cc98390ac775a93b469d2f3e0f9e138be5bd1e256cad5037c7183 src/muc_recovery.rs
|
||||
3252884252158e1c4bd58922b97733f434c7d28baf059e63267632a3290f660e src/object_store.rs
|
||||
7d7114a22ef3bf02f9caaf578f0a50d4f75b4dcd4b335b4cb8425d9716c97e71 src/pep_diagnostics.rs
|
||||
f09a6479b76c9c601a169b5f1f9cf1364e82df5ae27e4bab8d1b1428b5cad457 src/preauth.rs
|
||||
a7333b31a57780c57f2d30620cdfaeb7b1215808865960bb7e5b595d3ad98f7c src/protocol.rs
|
||||
761e29d6431fac5c2a4a24bcfca1f393949a5c55585fd6b0dd7a4f6e9f94a10d src/pubsub.rs
|
||||
0d7fec83ff188f9092b1945951f2879d846d5f6aef1e657671e8c4eeb4beb226 src/protocol.rs
|
||||
8dbb7a6b43a5f0562a775df42a959ab8badc466135a8b32da86cd79615b5acbf src/pubsub.rs
|
||||
913cd033f60a68f6c7cf9b3adbaf76db1c5c9bc88a0599196d373b16b64c0a1e src/push.rs
|
||||
bec477f2520af6a9d7ca54e43eb12158b53e3cf09057653e504dfdeab956885e src/sasl.rs
|
||||
10f24a2e8e776c7aa2475913e15b01b0f4e57f7a9de92660bafeddd912101d26 src/server.rs
|
||||
894f11389bfc3fddc3508396d106146261376999f7adcef5a049458f6b4028cf src/sessions.rs
|
||||
71d7ce1036a091d8c27016ea7ce4cffedf904dfca9d1cc4891b7dc66a1e06a4e src/storage.rs
|
||||
1bcc6995e221f316bd4765eeb79c2ad67c9fe9e93d4254e6f6679244fc733e6f src/server.rs
|
||||
c37f6c478d45b3c0741f0b1bed95f499e67d4d3eb50b78fe606e2b518170e45f src/session_store.rs
|
||||
1d4e890315886bc4fd0b824e034a5c1833a1f2c1347e8c3ca77254e13a2acab9 src/sessions.rs
|
||||
c9fb5d18da148a6366a18afb86eaee26a67d20c7b6664e425e612d44e79a2988 src/storage.rs
|
||||
462b00edb2c1e02dd34524bafe4f82b5ef902028b44310ba31b085d31f4dc2aa src/tls_binding.rs
|
||||
a78592aa79d8f01c22af1457d0c3f5922863ccd3e8ebb1aa05e043cc44579b8a src/upload_migration.rs
|
||||
f9b023565711e0217238b53fb678c5fb14849cdcd2efcd0fce249018274fad69 src/wire.rs
|
||||
b4eb7a230570dfde0fd0d301a6ace36559a5eae3e1402e42c76f014f61739c24 tests/_wire_client.py
|
||||
45613ad6c53087a50c6c3a7969b30783578c334499bc6baceadb2d18b0b3ea4e tests/admin_socket.rs
|
||||
0645fda01bede4362d998ef22aefdd2c02578dd2c2aae3878d0cff87b3679835 tests/android_sasl_network.py
|
||||
ad225ade463f55bfd0734e67f5306e274cd9aab59e10e91469bf7263547cdf79 tests/archive_edges.rs
|
||||
366b315521bd4f4448c0bac17bf74ddb77a728ff41fc48e29d46377625e0330f tests/archive_mam.rs
|
||||
df8530d69beaf36c34e78f0f050930b7e0d6a296c22ca543dfc7533180ba0543 tests/archive_redo.rs
|
||||
cb6b7bbcd1bc7ee01a4ccf892d4b6074765685c56c4b8bb46e0381b834577b6a tests/auth.rs
|
||||
011bbdd00c8ff8b648076d133086c395f6784cdcbb0c9f4e1efcbc15c1e79d9c tests/bind2_archive_latency_network.py
|
||||
b3c3470f0b9b64584abf98c26a4578b0b90089086d00f7b07187aa5f6db6a417 tests/cli.rs
|
||||
cc58adbb0fc867802b09cf7bd0d25397a273e721fbd0d0cb0ce30c19e80c13a2 tests/config.rs
|
||||
ba364e9ea561624b2c649cd0dda559e213c75e6d7080ae85f0c23f1f13d117e1 tests/csi.rs
|
||||
@@ -63,6 +80,7 @@ d4d8d982c6f4ab96a809b598ea9ead7f64fa9d916547f1eb9d797aa71db64fdb tests/data_loc
|
||||
be18a83fb5884cfdbd2ddc542cb25fd07b70f139cbdce1ae67562568df42a2e2 tests/debug_legacy.rs
|
||||
63753c2e3524e8e7826bc2d48e2bd78e36e1cc56fde9da19950d5c35e1c38f77 tests/federation_tls.rs
|
||||
1aaf06ba872cb5dbe0545afffdd8efddd0eae7305de06f3b3b75c19289581865 tests/fixtures/prosody.cfg.lua
|
||||
33a8a5d9980bb9aa1162a1e3e562a9d889edd3d2f96a7073e14acc8e504bf9c7 tests/fixtures/slow_sync.c
|
||||
19d570b92075d1355257c3798e4c52e404cc356dfe5dfd0c8fb384e4e7150682 tests/gajim_discovery_network.py
|
||||
4398bf0639c31f0df328f0b0f1a5df17f2e69d2df84514845bba882146de06bc tests/gajim_features.rs
|
||||
a346e70af3657fbf56e0bb1c39c427f630de8c349e6858b8933c52f42a21ab46 tests/http_altconnect.rs
|
||||
@@ -71,19 +89,27 @@ f1ea3eb78d5c716f4281cf17cb41b9fb22e08dbb4cf08ed9c8477c79b7d2382b tests/http_pro
|
||||
7c599c83eb999cd8776f0ce6811db8f478366b840311f2e513823f70b5d7cd30 tests/http_transport.rs
|
||||
278db32ba0dec882b0af8f1969da87670ff3d2d749d32320154dabef51cff191 tests/http_upload.rs
|
||||
da2fc65e931ec74fbcdcd26d04f2cb073e2dc1e354139135166c79580973a477 tests/iq_errors.rs
|
||||
ec993a6e31d38f82abd13bd46ad99286cf13e8abea63c0385c848037a12b0cd4 tests/journal_recovery_sync_network.py
|
||||
05160f17f8098466a67c9b069c165dc9d94a84ab03a49896169f110baf8e869a tests/logging.rs
|
||||
dfc3429e3e755d4c99ec1bd8ee6fb0f1f6aaf86414d5adaca7e9e3b18c7fe3f7 tests/logging_network.py
|
||||
0141961eadf9246ce287d7ee0dbbbf5243fd6e1332e09ccd2f6a591be419c2c3 tests/message_latency_network.py
|
||||
329ccbd4faee6cbbfa92d2fa0248844721b7ffc033baae4ec2570e9893f5df9f tests/message_routing.rs
|
||||
dafc2dd1e1d9e860482eb5bdf98c293842dea6a2790d27b94bf1885be3324690 tests/muc_access.rs
|
||||
c28c7e9128cd1483df90dbccc7af0d756a5edc813bdff604b4560a349a844754 tests/muc_domain_bans.rs
|
||||
b7d16e15e069649b9f0b055f0e0227117bb6d167c3a76cc713bd99fa19a6a60e tests/muc_extensions.rs
|
||||
59561f914274b437497ff35be00ed535ea012d18c99b589c8f944ea365b7112d tests/muc_flow.rs
|
||||
1134fefd5bb75884b096b8542c4f9745e64fd5dfcf87a6bba8d665aad2185a8a tests/muc_access.rs
|
||||
8120c66b85c0884619e39eea0b8761e11b41f2ed14c5be00146e35ca4714a9a0 tests/muc_crash_network.py
|
||||
168a8eb6ab892a379722dab0230efffb6585aaba639ad4691ec017626e1640d2 tests/muc_crash_state.rs
|
||||
2150cc7f4dccf8f1277783f5ccc94d6d626f8cb34ae6d4a05bf249863af29939 tests/muc_domain_bans.rs
|
||||
1d06b769b59012732ba04497a995f4f54df127f58c5f4f264a5df99589f31a94 tests/muc_extensions.rs
|
||||
a8a01faf51bf82bce6cbb2616cdaabb6337047d1c5f17d5f39975c0528a1d5ae tests/muc_flow.rs
|
||||
b24bd920f096576c2009ff1ccc0366eb8734c5f0b2feb39f04ad7087268a94b9 tests/muc_history.rs
|
||||
1f3df5f3f39143adc2b2a419fac0cd74b2229337ffa86570261d1f6ac61137a1 tests/muc_native.rs
|
||||
1b34ab721eec69295e1274e44bfd108c0e117dd383624cb6c98d851eb20e7a9a tests/muc_latency_storage.rs
|
||||
18f18362f26710043b51168675daee91ae88c071fb44e48210b49686011f8722 tests/muc_native.rs
|
||||
3df2736ccfd25a6cab290df80ceb7474ea44bb02af6cf768a9216d6b8f7f8630 tests/muc_optional_admin.rs
|
||||
5dc93407a658b45e504b89e26cfc8f495cd28ad90d0a308b7d228a472435ff9f tests/muc_protocol.rs
|
||||
ac1caa62a3cd91e74bd7100a5e542c404e97aec61a34e9627900b458c2ee3806 tests/muc_recovery.rs
|
||||
b5c2e179ee61287a8ec8b6bc593d8c960ca4f87cc71f2230e0c033fadd701ed3 tests/muc_restart_network.py
|
||||
e89a1ec8bfd4608795f6457710622b12a1520628184bc3f033cc6062e01c25d4 tests/muc_restart_state.rs
|
||||
13349e35ebaf65242cfb0bdbbccf0ea83acd80a1251312975141c2b74cccc1d7 tests/muc_state_native.rs
|
||||
5a742e53aa4707b4a060eeb26242006a34344ab57b8cad36668190a8dc5d0674 tests/muc_restart_state.rs
|
||||
7d8cc2b55bfccc92095c08b68dd6853b4ff2b7bfa460faf6c122e7fca09cca39 tests/muc_sm_load_network.py
|
||||
2e718aaa0328ac2a5f5bffbef5cd758bbfc49301d31edc77db316df8c7610acf tests/muc_state_native.rs
|
||||
cd13d1481dba7725986c47b5e5eb606241c8bbc32ab05d53f536f4431d6f7a3d tests/pep_client_interop.rs
|
||||
741cbcbf5552a8c3e5e7757a29de76ecd29da58f9d1184a35a075f1ed7aa5af9 tests/pep_client_network.py
|
||||
dd68c50d7d7cfd7871f69331a2787e0336dcab73762a82c599cee2a39920ae95 tests/pep_diagnostics.rs
|
||||
@@ -91,6 +117,7 @@ dd68c50d7d7cfd7871f69331a2787e0336dcab73762a82c599cee2a39920ae95 tests/pep_diag
|
||||
42b6c19d14e60b0c93193b4df473a0dd7cfcdb19250245ab94a7e1a237608395 tests/pep_item_order.rs
|
||||
3e349f9219da20602a239a6c4ce71dc045faf9afb1c36599e2b41e90b2423508 tests/pep_notifications_network.py
|
||||
83cea7164e5e6aadfc126b6b8349eb242bc113ba638385fa13c51ffda32aa57a tests/pep_order_network.py
|
||||
08d1a1c5585dba862b78deddab59bd45282610afa58852b29ef56f3023a79de9 tests/personal_message_latency_network.py
|
||||
6a078c3fbddff07131f3247d01fb1e13bc923888d93ea30175adda471cbece2f tests/preauth.rs
|
||||
b4048d34620bd0726cfb79d4d1cca9635f27ca8e58c8ad61d619af63672a2e09 tests/preauth_native.rs
|
||||
06a66caa73c179b9d3b239e6f4deac6fc876d8c8dfeafe302f043d85d9f4beb9 tests/preauth_network.py
|
||||
@@ -98,9 +125,10 @@ fa1eae49717ca9fb8b118f40058dff0c068c8f28823ea1b0ac42fd0c3e935bda tests/pubsub_n
|
||||
359de6dd2d960e63296f1109617f139bed2eac40409e50f72ff745897ba943ac tests/pubsub_persistence.rs
|
||||
6fb7475ca7359ad870a83d5f143a1994c7bd34e9b56e275f7496ba8da291ee17 tests/push.rs
|
||||
b8dd71798f96931f7ab4489cdb1e258556f41411815ef38d3a03e80e4e8fbdfc tests/push_discovery.py
|
||||
44c8e642f725be2292dfc5282feac8772ac34824927b63058f03f7c98285e5b6 tests/push_latency_network.py
|
||||
abf5e6a32ab542ccba5861eaaf7aed9a86b8306abfe2c329646d54e43176dc0c tests/reference/COPYING
|
||||
1ecd0a5c122910cbd066922c95ff0c32e945a07e2c9b0a37a30e5f035b5ecea6 tests/reference/configmanager.lua
|
||||
f407c6565fa73b6653e7e73204daa65ae1d87111e82c6b96154aea76497ad65d tests/reference_prosody.rs
|
||||
78fb639a334846c884f2d438407d7fa8491778b695f9e6218a161ebf8acaeced tests/reference_prosody.rs
|
||||
5ab78e9319d6004d65d35cd4d04dbb99d4ea5ffd8da00473d874bb26000fce35 tests/resource_rebind.py
|
||||
2e79de5ef8d3be81ac8f715bb96797b6e8e39cdcd61c13d8d873a96b53cb52e4 tests/resource_sessions.rs
|
||||
cefd1001c9619456402223fa89e449f65599c4397f523860e965ca192a4fd915 tests/sasl2_bind.py
|
||||
@@ -110,7 +138,13 @@ ddd9d5b46dc943138a334c64477fb11970b3d45e4cfbeaafc7348754f1e5e29e tests/sasl_acc
|
||||
a7d1519f4f44cc7bfad3d20bcfdf455e92abdd084feed493ffc9e0267a387740 tests/sasl_network.py
|
||||
4748fa227743686f78a0fa0364735d2dda5995f8d70a79ad03c1aa1decb42978 tests/sasl_retry_network.py
|
||||
d8882b8aabd6ff603c81fbbc259eefa68772afb2b3eec2e003ac29efd65d146b tests/sasl_web_network.py
|
||||
72f2d60e47e5295b82d497289b78925a64a1d986c70c277015ae228d211d32bb tests/storage_accounts.rs
|
||||
7d69c14ced7884450b9a1ff63cd984cdb9dfe948d6c56631c7593b6fabdabf7f tests/session_routing_isolation.rs
|
||||
7636b0767fdf24e3d4bde03779ec40309ed2b3301d4e35c6ae5f59aa065e4b2e tests/slow_storage_crash_network.py
|
||||
7492523bb226b68f72b97b028a7fafeb4fb5375b1efc6fb516400279dd0743d4 tests/slow_storage_latency_network.py
|
||||
d9eef5c37fcd99404619d61dbf7a2bf7673087924d93e69b08769e716afe297b tests/sm_durable_slots.rs
|
||||
b4c9422cdba8fd133572c0fa3a0ae6cda14edbd14db6d984d76f4409eefd1262 tests/sm_journal.rs
|
||||
8fa5de6a2291a7c35e7bbbc6334d42b4e0836a36bec252d0ef4703e8ee17aecb tests/sm_restart_network.py
|
||||
f76ad9de4c7e0140b1a4c3da06e849e28bb0562cf9b576ed85a5257f7394506b tests/storage_accounts.rs
|
||||
5c38afd5f77b5774552d12c461902ffaae21de11735560c6f0088e3dda8afa58 tests/storage_compat.rs
|
||||
8724991fdf22471952cf1a0fac863abe837069c93d3c8d2fa751af4f6bbc3e38 tests/tls_binding.rs
|
||||
5bea9b5546d3a6ae4fe78d17acbc3f6e6a1d5e1d13b32f2aa88d0e320892b7c5 tests/tls_plus_network.rs
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Проверка 0.8.0
|
||||
|
||||
Linux x86_64, Rust1.99.1, 2026-10-20. Итоговый полный прогон:
|
||||
[verify.txt](validation-logs-0.8.0/verify.txt).
|
||||
Структурированный отчёт: [validation-results.json](validation-results.json).
|
||||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| Rust tests | 293 passed, 0 failed, 11 ignored |
|
||||
| fmt / Clippy all-targets -D warnings / release | Успешно |
|
||||
| MUC SIGKILL через настоящий STARTTLS | temporary/persistent комнаты, несколько ресурсов, show/status/roles, порядок истории, подтверждённые leave/kick/demotion/password/config, фиксированный/custom grace — успешно |
|
||||
| SM между процессами | SIGKILL/SIGTERM, legacy SASL/SASL2, counters, ack/replay, неверные owner/token/h, resource conflict — успешно |
|
||||
| MUC offline fanout | До аутентификации Bob новое сообщение Alice уже в приватной durable SM-очереди; после resume доставка без history delay — успешно |
|
||||
| Отзыв SM | Удаление аккаунта подтверждается; после повторного создания старый SM-токен отклоняется |
|
||||
| MUC+SM нагрузка | 100 аккаунтов, 500 TLS-ресурсов, 10 комнат, 25000 join presence; SIGKILL и resume представителей всех комнат — успешно |
|
||||
| Native reference | 8 module/storage + 1 TLS/SASL EXTERNAL federation — успешно |
|
||||
| Остальные сетевые регрессии | Android SASL, WSS/BOSH/proxy/altconnect, PEP/push/preauth, resource policies, upload/S3/migration — успешно |
|
||||
|
||||
Нагрузка MUC+SM: connect 5.193 с; join 46.703 с;
|
||||
полный сценарий 78.857 с; выборочный peak RSS 41.26 MiB.
|
||||
Детали fanout, CPU, slots и replay: [muc-sm-load.json](validation-logs-0.8.0/muc-sm-load.json).
|
||||
Это loopback smoke measurement, а не гарантия production capacity.
|
||||
|
||||
SHA-256 итоговой release-сборки: `74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a`.
|
||||
Основной прогон: `XMPP_LOAD_CHECK=1 bash scripts/verify-local.sh`; native env описан
|
||||
в scripts/prosody_compat/README.md. В этой среде дополнительно применены
|
||||
CARGO_INCREMENTAL=0, CARGO_PROFILE_DEV_DEBUG=0, CARGO_PROFILE_TEST_DEBUG=0.
|
||||
|
||||
Первый основной прогон остановился на пятисекундном readiness timeout нового
|
||||
нагрузочного теста после успешных обычных и сетевых проверок. В самом тесте
|
||||
таймаут увеличен до60секунд; нагрузка500 повторена с той же release-сборкой,
|
||||
а native-проверки выполнены отдельно. verify.txt объединяет успешные части
|
||||
этих запусков; первоначальный лог сохранён в initial-load-timeout.txt.
|
||||
|
||||
В процессе проверки исправлены потеря сохранённых presence/history, защита
|
||||
истёкших recovery claims, отсутствие успешного ответа при удалении аккаунта,
|
||||
учёт stanza до SM enable и потеря MUC fanout до подключения другого участника.
|
||||
Первоначальный тест последнего случая принимал позднюю историю за доставку;
|
||||
итоговый тест проверяет именно private pending queue до аутентификации.
|
||||
|
||||
Реальные TCP/TLS-сокеты после завершения процесса отсутствуют: требуется новое
|
||||
защищённое аутентифицированное соединение. SM допускает повторную доставку при
|
||||
неопределённом сетевом результате; это не exactly-once транзакция между всеми
|
||||
хранилищами и сетью. Известные встроенные возможности MUC перенесены, произвольные
|
||||
community Lua-модули не исполняются. Пределы записей и порядок восстановления:
|
||||
[MUC-RECOVERY.md](MUC-RECOVERY.md). Полная замена всей установки и проверка
|
||||
реальными клиентами этим не заявляются: [COMPATIBILITY.md](COMPATIBILITY.md).
|
||||
|
||||
10GiB upload в0.8.0 не повторялся. Исторические испытания local/MinIO находятся
|
||||
в VALIDATION-0.7.8.md и VALIDATION-0.7.9.md; их результаты не приписываются новой сборке.
|
||||
@@ -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).
|
||||
@@ -0,0 +1,79 @@
|
||||
# Проверка 0.8.3
|
||||
|
||||
2026-10-21, Linux x86_64, Rust 1.99.1. Итоговый единый прогон после заморозки
|
||||
исходников: [verify.txt](validation-logs-0.8.3/verify.txt).
|
||||
[Machine-readable results](validation-results-0.8.3.json).
|
||||
|
||||
|Проверка|Результат|
|
||||
|---|---|
|
||||
|Rust ordinary tests|334 passed, 0 failed, 11 ignored|
|
||||
|fmt / Clippy all-targets -D warnings / release|Успешно|
|
||||
|Новые Rust regressions|13 archive redo + 2 rewrite crash boundaries + 14 SM journal, успешно|
|
||||
|Native reference|8 module/storage + 1 TLS/SASL EXTERNAL federation, успешно|
|
||||
|Archive >128 MiB|Отдельно выполненный ignored streaming test, успешно|
|
||||
|Copy/restore и rewrite|Новый inode, потеря suffix, baseline0/nonzero, crash до/после rename — успешно|
|
||||
|Все network regressions verify-local.sh|Успешно, включая Android, WSS/BOSH, PEP, push, MUC crash/SM resume, upload/migration|
|
||||
|Slow durable IO|0/100 мс: 3 samples/scenario; 200 мс: 1 sample/scenario; exact MAM/carbons/queues и phase logs проверены|
|
||||
|SIGKILL сразу после sender SM ACK|100 мс/sync, 3 сообщения, оба MAM архива, 4 resume, 9 replay, до reauth; успешно|
|
||||
|Recovery sync EIO|WAL/redo: отказ handoff без удаления journal; WAL startup до listeners; retry и данные проверены|
|
||||
|Offline storage-checkpoint|4 SM exports, 2 archives, durable journal removal, повтор идемпотентен|
|
||||
|Общая нагрузка|500 TLS, 100 accounts, 5000 ping, 5000 messages, 10000 MAM, failures=[]|
|
||||
|MUC+SM нагрузка|500 resources, 10 rooms, 25000 presence, 1500 deliveries, SIGKILL и 10 resume; успешно|
|
||||
|
||||
## Личная доставка
|
||||
|
||||
Медианы для 3 samples, при 200 мс — одна проба; числа в миллисекундах.
|
||||
|
||||
|Sync delay / сценарий|Body 0.8.2 → 0.8.3|Sender SM ACK 0.8.2 → 0.8.3|Проб|
|
||||
|---|---:|---:|---:|
|
||||
|0 мс / 1:1|25.11 → 7.87|33.41 → 15.08|3|
|
||||
|0 мс / PEP → 1:1|29.71 → 20.94|32.25 → 21.86|3|
|
||||
|100 мс / 1:1|1232.47 → 419.68|1641.60 → 644.08|3|
|
||||
|100 мс / PEP → 1:1|2683.29 → 1378.23|3195.21 → 1581.98|3|
|
||||
|200 мс / 1:1|2836.64 → 2042.56|3643.30 → 2441.99|1|
|
||||
|200 мс / PEP → 1:1|5782.02 → 2676.28|6595.92 → 3094.95|1|
|
||||
|
||||
0 мс — отдельная последовательная quiet pair, без full verify. Финальные
|
||||
100/200 мс выполнялись при фоновой работе verify; это не изолированный CPU benchmark.
|
||||
Первый append включает создание/migration MAM journal. В обычном warm сообщении
|
||||
6 sync вместо 16; первый — 12. Окна счётчиков могут включать heartbeat, а PEP
|
||||
сценарий включает предшествующую publication. Реальные fsync/fdatasync сохранены;
|
||||
interposer замедляет только isolated data_path, не меняет host mount/DNS.
|
||||
Raw samples, hashes, sync counts и phase fields — validation-logs-0.8.3/*.json.
|
||||
|
||||
В production log 0.8.2 серверный handler занимал 4792–5549 мс; старых полей
|
||||
недостаточно, чтобы доказать конкретный IO stage. Новый таймер разделяет MAM,
|
||||
доставку, carbons и handled, без payload. Эти loopback результаты не доказывают
|
||||
время отображения реальным Conversations/Monocles на wthome.ru.
|
||||
|
||||
## Нагрузка и сопоставимость
|
||||
|
||||
Сопоставимый повтор 0.8.2 в текущей среде: total 151.334 с,
|
||||
join 104.226 с. Финальный 0.8.3: total 128.380 с,
|
||||
join 94.493 с, restart 7.557 с,
|
||||
peak RSS 40.87 MiB. По одной пробе; фоновые условия
|
||||
различаются, вывод о CPU efficiency не делается. Исторические 45.54 с из другой
|
||||
среды не являются baseline этого сравнения. Все счётчики доставки и replay
|
||||
проверены, а не только открытие порта.
|
||||
|
||||
## Durability и native handoff
|
||||
|
||||
SM WAL и archive redo авторитетны после аварии. Recovery durably adopts полный
|
||||
валидный journal перед публикацией; EIO regression выявляла отсутствие barrier
|
||||
в предварительном кандидате a5f5b3a…, теперь проходит на final binary. Отчёты
|
||||
preliminary-recovery-sync-* сохранены именно как предварительные, не результаты
|
||||
0.8.2 или final. Изначальный native MUC fixture делал in-place append без передачи
|
||||
владения: теперь он явно вызывает checkpoint_all перед оригинальным Prosody.
|
||||
Неjournaled suffix при активном redo намеренно не объявляется external commit.
|
||||
Copy/restore с новым inode сохраняет авторитет redo; внутренний rewrite
|
||||
фиксирует старый native и удаление redo перед atomic replacement. Проверены
|
||||
оба crash boundaries, потеря suffix после копирования и обе baseline длины.
|
||||
Перед native writer/откатом требуется остановка и `storage-checkpoint CONFIG`.
|
||||
[Формат, ограничения и эксплуатация](DURABLE-MESSAGE-IO.md).
|
||||
|
||||
Release SHA-256: `8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a`.
|
||||
Один final verify с XMPP_LOAD_CHECK=1 и native env; отдельно archive ignored test
|
||||
и подробные latency/crash/EIO fixtures. Исторические результаты сохранены.
|
||||
Оставшийся AF_UNIX ignored test не запускался; 10 GiB upload и реальный MinIO/AWS,
|
||||
Docker build/run, GitHub/Gitea publish и настоящие APNs/FCM не повторялись.
|
||||
Общие границы полной совместимости: [COMPATIBILITY.md](COMPATIBILITY.md).
|
||||
+69
-49
@@ -1,59 +1,79 @@
|
||||
# Проверка 0.7.9
|
||||
# Проверка 0.8.3
|
||||
|
||||
Linux x86_64, Rust 1.99.1, 2026-10-20. Полный итоговый прогон:
|
||||
validation-logs-0.7.9/verify.txt. Структурированный отчёт: validation-results.json.
|
||||
2026-10-21, Linux x86_64, Rust 1.99.1. Итоговый единый прогон после заморозки
|
||||
исходников: [verify.txt](validation-logs-0.8.3/verify.txt).
|
||||
[Machine-readable results](validation-results-0.8.3.json).
|
||||
|
||||
| Проверка | Результат |
|
||||
|Проверка|Результат|
|
||||
|---|---|
|
||||
| cargo test --locked | 265 passed, 0 failed, 11 ignored |
|
||||
| fmt / Clippy all-targets -D warnings / release | Успешно |
|
||||
| Upload migration | local → S3 A → S3 B → local, SHA-256, прежние URL/JWT/HEAD, native metadata/source, dry run, lock, прерывание/resume и startup barrier — успешно |
|
||||
| S3 recovery | Intent до initiation, отсутствие UploadId, pagination и exact-key cleanup без удаления чужого prefix, ambiguous completion, quarantine и late commit — успешно |
|
||||
| Конкурентный PUT | Регрессия: захват слота читает свежие completion/quarantine flags атомарно — успешно |
|
||||
| Реальная передача через MinIO | 10,737,418,240 байт PUT и полный GET; SHA-256 совпал; объект: 1280 multipart parts |
|
||||
| Время большого файла | PUT 48.453 с; GET 13.710 с |
|
||||
| Выборочный RSS каждые 50 мс | Rust 101.14 MiB; клиент 23.21 MiB; MinIO 475.84 MiB |
|
||||
| MinIO дополнительные проверки | Range за 4/5 GiB и хвост, restart/read/reuse409, SIGKILL после сохранённой части и abort при recovery — успешно |
|
||||
| Native reference | 8 module/storage + 1 TLS/SASL EXTERNAL federation — успешно |
|
||||
| Нагрузка | 100 аккаунтов, 500 TLS клиентов, 5000 ping,5000 сообщений, 10000 MAM; failures=[] |
|
||||
|Rust ordinary tests|334 passed, 0 failed, 11 ignored|
|
||||
|fmt / Clippy all-targets -D warnings / release|Успешно|
|
||||
|Новые Rust regressions|13 archive redo + 2 rewrite crash boundaries + 14 SM journal, успешно|
|
||||
|Native reference|8 module/storage + 1 TLS/SASL EXTERNAL federation, успешно|
|
||||
|Archive >128 MiB|Отдельно выполненный ignored streaming test, успешно|
|
||||
|Copy/restore и rewrite|Новый inode, потеря suffix, baseline0/nonzero, crash до/после rename — успешно|
|
||||
|Все network regressions verify-local.sh|Успешно, включая Android, WSS/BOSH, PEP, push, MUC crash/SM resume, upload/migration|
|
||||
|Slow durable IO|0/100 мс: 3 samples/scenario; 200 мс: 1 sample/scenario; exact MAM/carbons/queues и phase logs проверены|
|
||||
|SIGKILL сразу после sender SM ACK|100 мс/sync, 3 сообщения, оба MAM архива, 4 resume, 9 replay, до reauth; успешно|
|
||||
|Recovery sync EIO|WAL/redo: отказ handoff без удаления journal; WAL startup до listeners; retry и данные проверены|
|
||||
|Offline storage-checkpoint|4 SM exports, 2 archives, durable journal removal, повтор идемпотентен|
|
||||
|Общая нагрузка|500 TLS, 100 accounts, 5000 ping, 5000 messages, 10000 MAM, failures=[]|
|
||||
|MUC+SM нагрузка|500 resources, 10 rooms, 25000 presence, 1500 deliveries, SIGKILL и 10 resume; успешно|
|
||||
|
||||
Итоговая сборка SHA-256: 24c0dca637e7ce4e238ef6becad16bb5a18a33546554b0eecf310fd8927f021c. MinIO report автоматически записывает
|
||||
тот же hash. Большая передача повторена после последних изменений сервера.
|
||||
Полный прогон включает исправление гонки устаревшего snapshot при захвате слота.
|
||||
Отдельные первые прогоны нашли и помогли исправить нестабильный fingerprint
|
||||
миграции и пропуск немедленного reconciliation под ещё активным claim.
|
||||
Финальный результат не содержит этих ошибок. RSS — выборочный максимум, а не
|
||||
гарантия точного пика для любой нагрузки. Временные 10 GiB объекты после проверки удалены.
|
||||
## Личная доставка
|
||||
|
||||
## Точная граница MinIO проверки
|
||||
Медианы для 3 samples, при 200 мс — одна проба; числа в миллисекундах.
|
||||
|
||||
Использован настоящий upstream MinIO RELEASE.2025-04-22T22-12-26Z, собранный
|
||||
Go1.24.3 из официального GitHub archive. Немодифицированная сборка в этой среде
|
||||
не запускается: net.Interfaces/netlinkrib запрещён с EPERM. Тестовая сборка
|
||||
добавляет только fallback на 127.0.0.1 для этой ошибки перечисления интерфейсов.
|
||||
S3, authentication, multipart и storage code не изменены. Это не qualification
|
||||
немодифицированного MinIO в обычной production среде.
|
||||
|Sync delay / сценарий|Body 0.8.2 → 0.8.3|Sender SM ACK 0.8.2 → 0.8.3|Проб|
|
||||
|---|---:|---:|---:|
|
||||
|0 мс / 1:1|25.11 → 7.87|33.41 → 15.08|3|
|
||||
|0 мс / PEP → 1:1|29.71 → 20.94|32.25 → 21.86|3|
|
||||
|100 мс / 1:1|1232.47 → 419.68|1641.60 → 644.08|3|
|
||||
|100 мс / PEP → 1:1|2683.29 → 1378.23|3195.21 → 1581.98|3|
|
||||
|200 мс / 1:1|2836.64 → 2042.56|3643.30 → 2441.99|1|
|
||||
|200 мс / PEP → 1:1|5782.02 → 2676.28|6595.92 → 3094.95|1|
|
||||
|
||||
Точный diff, hashes исходного архива, обеих сборок и adapter:
|
||||
validation-logs-0.7.9/minio-sandbox-compat.patch и minio-provenance.json.
|
||||
Полный результат и 1280-part ETag: validation-logs-0.7.9/minio.json.
|
||||
Проверен single-node loopback с HTTP к MinIO и CA-verified HTTPS к gateway.
|
||||
AWS account, distributed MinIO и production сеть не проверены.
|
||||
0 мс — отдельная последовательная quiet pair, без full verify. Финальные
|
||||
100/200 мс выполнялись при фоновой работе verify; это не изолированный CPU benchmark.
|
||||
Первый append включает создание/migration MAM journal. В обычном warm сообщении
|
||||
6 sync вместо 16; первый — 12. Окна счётчиков могут включать heartbeat, а PEP
|
||||
сценарий включает предшествующую publication. Реальные fsync/fdatasync сохранены;
|
||||
interposer замедляет только isolated data_path, не меняет host mount/DNS.
|
||||
Raw samples, hashes, sync counts и phase fields — validation-logs-0.8.3/*.json.
|
||||
|
||||
## Повторение
|
||||
В production log 0.8.2 серверный handler занимал 4792–5549 мс; старых полей
|
||||
недостаточно, чтобы доказать конкретный IO stage. Новый таймер разделяет MAM,
|
||||
доставку, carbons и handled, без payload. Эти loopback результаты не доказывают
|
||||
время отображения реальным Conversations/Monocles на wthome.ru.
|
||||
|
||||
Полный прогон: XMPP_LOAD_CHECK=1 bash scripts/verify-local.sh с native env
|
||||
согласно scripts/prosody_compat/README.md. В этой среде дополнительно применены
|
||||
CARGO_INCREMENTAL=0 CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0.
|
||||
MinIO отдельно: python3 tests/upload_minio_network.py --binary target/release/rust-xmpp
|
||||
--minio /path/to/minio --report /tmp/minio-report.json. При test adapter передайте
|
||||
--minio-compat-patch /path/to/patch, чтобы он попал в результат. Можно задать
|
||||
MINIO_BINARY перед verify-local.sh для opt-in передачи10 GiB; нужны место и MinIO executable.
|
||||
## Нагрузка и сопоставимость
|
||||
|
||||
Инструкции migration/recovery: HTTP-UPLOAD.md. Source всегда сохраняется;
|
||||
незавершённая миграция блокирует startup, неопределённый Complete quarantines
|
||||
только свой slot. Потеря ownership metadata не позволяет безопасно удалить
|
||||
произвольные чужие multiparts; bucket lifecycle остаётся дополнительной защитой.
|
||||
Сетевой сбой с HEAD404 не выдаётся за доказанную неудачу удалённого commit.
|
||||
Полная замена Prosody, настоящие production клиенты и DNSSEC этим не заявляются;
|
||||
общие границы — COMPATIBILITY.md.
|
||||
Сопоставимый повтор 0.8.2 в текущей среде: total 151.334 с,
|
||||
join 104.226 с. Финальный 0.8.3: total 128.380 с,
|
||||
join 94.493 с, restart 7.557 с,
|
||||
peak RSS 40.87 MiB. По одной пробе; фоновые условия
|
||||
различаются, вывод о CPU efficiency не делается. Исторические 45.54 с из другой
|
||||
среды не являются baseline этого сравнения. Все счётчики доставки и replay
|
||||
проверены, а не только открытие порта.
|
||||
|
||||
## Durability и native handoff
|
||||
|
||||
SM WAL и archive redo авторитетны после аварии. Recovery durably adopts полный
|
||||
валидный journal перед публикацией; EIO regression выявляла отсутствие barrier
|
||||
в предварительном кандидате a5f5b3a…, теперь проходит на final binary. Отчёты
|
||||
preliminary-recovery-sync-* сохранены именно как предварительные, не результаты
|
||||
0.8.2 или final. Изначальный native MUC fixture делал in-place append без передачи
|
||||
владения: теперь он явно вызывает checkpoint_all перед оригинальным Prosody.
|
||||
Неjournaled suffix при активном redo намеренно не объявляется external commit.
|
||||
Copy/restore с новым inode сохраняет авторитет redo; внутренний rewrite
|
||||
фиксирует старый native и удаление redo перед atomic replacement. Проверены
|
||||
оба crash boundaries, потеря suffix после копирования и обе baseline длины.
|
||||
Перед native writer/откатом требуется остановка и `storage-checkpoint CONFIG`.
|
||||
[Формат, ограничения и эксплуатация](DURABLE-MESSAGE-IO.md).
|
||||
|
||||
Release SHA-256: `8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a`.
|
||||
Один final verify с XMPP_LOAD_CHECK=1 и native env; отдельно archive ignored test
|
||||
и подробные latency/crash/EIO fixtures. Исторические результаты сохранены.
|
||||
Оставшийся AF_UNIX ignored test не запускался; 10 GiB upload и реальный MinIO/AWS,
|
||||
Docker build/run, GitHub/Gitea publish и настоящие APNs/FCM не повторялись.
|
||||
Общие границы полной совместимости: [COMPATIBILITY.md](COMPATIBILITY.md).
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,11 @@
|
||||
PASS: recovery-history
|
||||
PASS: leave
|
||||
PASS: kick
|
||||
PASS: demotion
|
||||
PASS: password
|
||||
PASS: configuration
|
||||
PASS: restart-grace
|
||||
PASS: custom-grace
|
||||
PASS: unknown-module
|
||||
PASS: destroy-tombstone
|
||||
PASS: actual STARTTLS SIGKILL MUC recovery, history, acknowledged barriers and fixed grace
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"clients": 500,
|
||||
"accounts": 100,
|
||||
"rooms": 10,
|
||||
"message_rounds": 3,
|
||||
"concurrency": 20,
|
||||
"binary": "/tmp/xmpp-durable-muc-load-g0crm4ik/rust-xmpp",
|
||||
"binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
|
||||
"before": {
|
||||
"rss_kib": 9696,
|
||||
"cpu_seconds": 0.0,
|
||||
"fd_count": 12
|
||||
},
|
||||
"connect_seconds": 5.193,
|
||||
"connect_latency": {
|
||||
"samples": 500,
|
||||
"median_ms": 204.391,
|
||||
"p95_ms": 270.035,
|
||||
"max_ms": 332.503
|
||||
},
|
||||
"phase": "completed",
|
||||
"status": "pass",
|
||||
"elapsed_seconds": 78.857,
|
||||
"join_seconds": 46.703,
|
||||
"joined": {
|
||||
"rss_kib": 42248,
|
||||
"cpu_seconds": 17.77,
|
||||
"fd_count": 513
|
||||
},
|
||||
"join_presence_deliveries": 25000,
|
||||
"slots_joined": {
|
||||
"count": 500,
|
||||
"bytes": 309790
|
||||
},
|
||||
"message_seconds": 10.861,
|
||||
"room_messages_sent": 30,
|
||||
"room_messages_delivered": 1500,
|
||||
"before_kill": {
|
||||
"rss_kib": 42128,
|
||||
"cpu_seconds": 23.12,
|
||||
"fd_count": 514
|
||||
},
|
||||
"slots_before_kill": {
|
||||
"count": 500,
|
||||
"bytes": 514030
|
||||
},
|
||||
"restart_seconds": 10.559,
|
||||
"resumed_resources": 10,
|
||||
"replayed_pending_room_messages": 10,
|
||||
"membership_groupchat_deliveries_after_resume": 10,
|
||||
"after_resume": {
|
||||
"rss_kib": 21168,
|
||||
"cpu_seconds": 5.03,
|
||||
"fd_count": 23
|
||||
},
|
||||
"slots_after_resume": {
|
||||
"count": 500,
|
||||
"bytes": 671070
|
||||
},
|
||||
"total_seconds": 78.857,
|
||||
"peak_rss_kib": 42252
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
PASS: TLS legacy/SASL2 durable SM after SIGKILL/SIGTERM, counters, ack pruning, at-least-once pending replay, owner/token/h rejection, transport/resource conflict, MUC activation/offline fanout, grace expiry and account deletion/recreation revocation
|
||||
@@ -0,0 +1,846 @@
|
||||
Finished `test` profile [unoptimized] target(s) in 0.09s
|
||||
Running unittests src/lib.rs (target/debug/deps/rust_xmpp-c18b5fae509f7258)
|
||||
|
||||
running 46 tests
|
||||
test federation::tests::federation_domains_are_dns_names_and_case_insensitive ... ok
|
||||
test federation::tests::starttls_must_be_an_immediate_stream_feature ... ok
|
||||
test federation::tests::external_must_be_in_the_advertised_mechanisms ... ok
|
||||
test http_config::tests::external_prefix_and_standard_port ... ok
|
||||
test http_config::tests::invalid_paths_types_urls_and_conflicts_are_errors ... ok
|
||||
test federation::tests::stream_restarts_compare_canonical_domains ... ok
|
||||
test http_config::tests::mounts_inherit_per_app_and_expand_virtual_host ... ok
|
||||
test http_config::tests::literal_pchars_and_percent_encoding_are_supported ... ok
|
||||
test http_config::tests::routes_hosts_and_default_without_overriding_known_hosts ... ok
|
||||
test http_proxy::tests::config_validation_and_network_boundaries ... ok
|
||||
test http_proxy::tests::empty_native_lua_table_disables_proxy_trust ... ok
|
||||
test http_proxy::tests::legacy_chain_uses_rightmost_untrusted_and_tls_is_preserved ... ok
|
||||
test http_proxy::tests::untrusted_socket_ignores_spoofed_headers ... ok
|
||||
test http_proxy::tests::malformed_forwarded_and_unknown_identifiers_are_ignored ... ok
|
||||
test http_proxy::tests::forwarded_mode_stops_at_first_untrusted_hop ... ok
|
||||
test http_proxy::tests::forwarded_tls_termination_cannot_upgrade_a_plaintext_chain_hop ... ok
|
||||
test http_transport::tests::bosh_processing_capacity_is_bounded ... ok
|
||||
test http_transport::tests::independent_framing ... ok
|
||||
test http_transport::tests::websocket_preserves_inherited_namespaces_and_spaced_declarations ... ok
|
||||
test logging::tests::console_aliases_share_one_sink_with_combined_levels ... ok
|
||||
test http_transport::tests::configured_mount_host_origin_and_secure_dispatch ... ok
|
||||
test sasl::tests::reject_malformed_and_downgrade ... ok
|
||||
test sasl::tests::rfc5802_sha1_vector ... ok
|
||||
test server::authorization_tests::http_interfaces_follow_secure_override_wildcards_and_global_fallback ... ok
|
||||
test server::authorization_tests::pep_diagnostics_count_only_first_item_devices_without_logging_ids ... ok
|
||||
test server::authorization_tests::pep_diagnostics_exclude_payloads_and_identifiers ... ok
|
||||
test http_service::tests::upload_claim_refreshes_completion_state_after_an_earlier_snapshot ... ok
|
||||
test sessions::push_timeout_tests::first_important_push_shortens_extended_hibernation_once ... ok
|
||||
test sessions::tests::acknowledgement_wraps ... ok
|
||||
test sessions::tests::graceful_disconnect_is_not_resumable ... ok
|
||||
test sessions::tests::resume_replays_only_unacked_and_checks_owner ... ok
|
||||
test sessions::tests::traffic_before_sm_enable_is_not_counted ... ok
|
||||
test upload_migration::tests::local_spool_checks_size_and_hash_and_preserves_source ... ok
|
||||
test upload_migration::tests::pending_and_corrupt_journals_block_startup ... ok
|
||||
test wire::tests::arbitrary_prefix_and_inherited_namespaces ... ok
|
||||
test wire::tests::cdata_and_comments_frame_without_counting_brackets ... ok
|
||||
test wire::tests::declarations_rejected ... ok
|
||||
test wire::tests::fragmented_and_pipelined ... ok
|
||||
test wire::tests::invalid_namespace_and_declarations_rejected ... ok
|
||||
test http_service::tests::upload_internal_mount_is_independent_of_advertised_proxy_prefix ... ok
|
||||
test sasl::tests::sha256_independent_python_vector ... ok
|
||||
test http_service::tests::abandoned_slots_release_quota_without_pruning_existing_or_active_files ... ok
|
||||
test sasl::tests::plus_channel_binding_and_ssdp_are_authenticated ... ok
|
||||
test server::authorization_tests::upload_requires_registered_local_account_or_explicit_remote_access ... ok
|
||||
test sasl::tests::wrong_proof_nonce_binding_and_duplicates ... ok
|
||||
test admin::tests::real_admin_xml_protocol_over_duplex ... ok
|
||||
|
||||
test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.59s
|
||||
|
||||
Running unittests src/main.rs (target/debug/deps/rust_xmpp-c4e45dd69a11a11d)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/admin_socket.rs (target/debug/deps/admin_socket-61170eb8eabf7ff1)
|
||||
|
||||
running 1 test
|
||||
test prosody_wire_commands_password_prompt_and_safe_errors ... ignored, requires AF_UNIX socket permission; run on target host with --ignored
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/archive_mam.rs (target/debug/deps/archive_mam-66e5d9709846757b)
|
||||
|
||||
running 14 tests
|
||||
test archive_larger_than_128mib_streams ... ignored, large-file scalability regression; run explicitly
|
||||
test corrupt_archive_reports_server_error_and_namespace_stripping_is_precise ... ok
|
||||
test mam_preferences_persist ... ok
|
||||
test legacy_records_binary_cache_and_roundtrip ... ok
|
||||
test append_is_incremental_and_recovers_torn_tail ... ok
|
||||
test archive_roundtrip_preserves_default_namespace_resets ... ok
|
||||
test equal_cursors_and_normalized_query_jids ... ok
|
||||
test muc_archive_filters_stanza_kind_instead_of_sender_jid ... ok
|
||||
test smart_enable_and_message_selection ... ok
|
||||
test real_archive_sid_and_sender_privacy ... ok
|
||||
test zero_page_dates_and_invalid_forms ... ok
|
||||
test mam_paging_filters_and_forbidden ... ok
|
||||
test extended_ids_ranges_flip_and_metadata ... ok
|
||||
test namespaces_expiry_and_concurrent_append ... ok
|
||||
|
||||
test result: ok. 13 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.04s
|
||||
|
||||
Running tests/auth.rs (target/debug/deps/auth-3fecc5005dc5e6c7)
|
||||
|
||||
running 3 tests
|
||||
test traversal_and_malicious_account_records_rejected ... ok
|
||||
test create_and_read_hashed_accounts ... ok
|
||||
test authentication_prepares_names_without_relaxing_account_management ... ok
|
||||
|
||||
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.11s
|
||||
|
||||
Running tests/cli.rs (target/debug/deps/cli-a1c004101734b0cf)
|
||||
|
||||
running 2 tests
|
||||
test server_cannot_be_mistaken_for_running_service ... ok
|
||||
test conversion_preserves_source_and_refuses_overwrite ... ok
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
|
||||
|
||||
Running tests/config.rs (target/debug/deps/config-bb0cdd36fb8d5839)
|
||||
|
||||
running 12 tests
|
||||
test include_cycles_rejected ... ok
|
||||
test component_can_explicitly_enable_global_modules ... ok
|
||||
test storage_paths_encode_like_prosody ... ok
|
||||
test include_has_own_global_scope_and_parent_scope_is_restored ... ok
|
||||
test declarations_and_overrides ... ok
|
||||
test tables_cannot_recurse_forever ... ok
|
||||
test supplied_configuration_scopes_arithmetic_and_modules ... ok
|
||||
test runtime_capabilities_are_unavailable ... ok
|
||||
test all_formats_round_trip_actual_config ... ok
|
||||
test supplied_config_matches_original_prosody_loader ... ok
|
||||
test malformed_values_rejected ... ok
|
||||
test loops_are_bounded ... ok
|
||||
|
||||
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
|
||||
|
||||
Running tests/csi.rs (target/debug/deps/csi-ef5a8b30cab0cbf7)
|
||||
|
||||
running 2 tests
|
||||
test bounded_queue_flush_delay_activity_and_grace ... ok
|
||||
test native_importance_rules ... ok
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/csi_native.rs (target/debug/deps/csi_native-7a0bbbead7a19df9)
|
||||
|
||||
running 1 test
|
||||
test native_prosody_csi_importance_matches ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/data_lock.rs (target/debug/deps/data_lock-897044c51991a534)
|
||||
|
||||
running 2 tests
|
||||
test child_process_probe ... ok
|
||||
test excludes_another_process_and_releases_on_drop ... ok
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/debug_legacy.rs (target/debug/deps/debug_legacy-3c721d03a5eecf02)
|
||||
|
||||
running 1 test
|
||||
test debug_empty_legacy_bind ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s
|
||||
|
||||
Running tests/federation_tls.rs (target/debug/deps/federation_tls-6dc128df24b929d8)
|
||||
|
||||
running 3 tests
|
||||
test native_prosody_tls_external_bidirectional ... ignored, requires native Prosody cafd74b9c5ea and Lua TLS dependencies
|
||||
test mutual_tls_external_delivery_and_certificate_identity_rejection ... ok
|
||||
test reuses_connections_serializes_writes_and_retires_idle_or_closed_peers ... ok
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 1.48s
|
||||
|
||||
Running tests/gajim_features.rs (target/debug/deps/gajim_features-f6c3a49b9451f82c)
|
||||
|
||||
running 6 tests
|
||||
test policy_catalog_selects_default_policy_and_maps_display_and_explicit_label ... ok
|
||||
test security_catalog_default_is_a_real_label_and_module_can_be_disabled ... ok
|
||||
test actual_example_policy_configuration_exposes_classifications_as_catalog_labels ... ok
|
||||
test unsupported_security_policy_configuration_returns_explicit_error ... ok
|
||||
test security_catalogs_serve_both_versions_and_preserve_requested_target ... ok
|
||||
test malformed_or_unknown_policy_catalog_schemas_are_rejected ... ok
|
||||
|
||||
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/http_altconnect.rs (target/debug/deps/http_altconnect-9136542484c4f432)
|
||||
|
||||
running 3 tests
|
||||
test native_link_order_schemes_and_loaded_module_gates ... ok
|
||||
test public_prefix_and_host_paths_inherit_independently ... ok
|
||||
test bad_advertisement_types_and_route_collisions_are_rejected ... ok
|
||||
|
||||
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/http_transport.rs (target/debug/deps/http_transport-ba31449c82cc1657)
|
||||
|
||||
running 10 tests
|
||||
test configured_origin_is_enforced ... ok
|
||||
test bosh_preserves_namespace_inherited_from_body ... ok
|
||||
test transport_paths_and_cors_preflight_match_prosody ... ok
|
||||
test bosh_session_retries_restart_termination ... ok
|
||||
test bosh_termination_waits_for_graceful_cleanup_before_reconnect ... ok
|
||||
test bosh_reorders_two_requests_without_replaying_stanzas ... ok
|
||||
test websocket_frames_are_independent_xml ... ok
|
||||
test websocket_close_allows_graceful_engine_cleanup ... ok
|
||||
test websocket_disconnect_delivers_eof_without_graceful_close ... ok
|
||||
test bosh_retry_survives_aborted_http_request_without_replaying_stanzas ... ok
|
||||
|
||||
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
|
||||
|
||||
Running tests/http_upload.rs (target/debug/deps/http_upload-ff5554e98fd3b948)
|
||||
|
||||
running 5 tests
|
||||
test https_endpoint_uses_real_tls ... ok
|
||||
test percent_escape_case_and_equivalent_url_spelling ... ok
|
||||
test persisted_prosody_archive_and_hs256_slot_survive_without_sidecar ... ok
|
||||
test expired_upload_releases_quota ... ok
|
||||
test upload_authorization_streaming_ranges_and_restart ... ok
|
||||
|
||||
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
|
||||
|
||||
Running tests/iq_errors.rs (target/debug/deps/iq_errors-f6728f43bbee20cf)
|
||||
|
||||
running 2 tests
|
||||
test pep_errors_identify_the_account_and_echo_the_request ... ok
|
||||
test server_errors_without_target_remain_unaddressed_and_escape_attributes ... ok
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/logging.rs (target/debug/deps/logging-49936a1290966f86)
|
||||
|
||||
running 10 tests
|
||||
test native_console_shorthands_are_accepted ... ok
|
||||
test rejects_malformed_configuration ... ok
|
||||
test json_records_and_bounded_ranges ... ok
|
||||
test failed_reopen_keeps_all_original_handles ... ok
|
||||
test filters_appends_escapes_and_redacts ... ok
|
||||
test reopen_rotated_files_updates_clones ... ok
|
||||
test explicit_sink_levels_select_exactly_and_deduplicate ... ok
|
||||
test shared_sink_unions_levels_and_preserves_distinct_destinations ... ok
|
||||
test overlapping_shorthand_destinations_receive_each_record_once ... ok
|
||||
test cloned_loggers_serialize_records ... ok
|
||||
|
||||
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
|
||||
|
||||
Running tests/message_routing.rs (target/debug/deps/message_routing-e8cb56e9c75084cc)
|
||||
|
||||
running 2 tests
|
||||
test unknown_resource_falls_back_to_available_bare_jid ... ok
|
||||
test headline_uses_all_nonnegative_available_resources_and_errors_never_fallback ... ok
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/muc_access.rs (target/debug/deps/muc_access-3d45cfc19a946c63)
|
||||
|
||||
running 3 tests
|
||||
test binary_occupant_id_salt_survives_saves_and_restarts ... ok
|
||||
test moderators_cannot_kick_or_demote_other_moderators ... ok
|
||||
test nested_affiliations_do_not_fall_back_to_obsolete_flat_entries ... ok
|
||||
|
||||
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
|
||||
|
||||
Running tests/muc_crash_state.rs (target/debug/deps/muc_crash_state-f6de95e841b62f79)
|
||||
|
||||
running 7 tests
|
||||
test safe_presence_checkpoint_sanitization_is_idempotent ... ok
|
||||
test foreign_stanzas_do_not_initialize_or_rewrite_muc_checkpoint ... ok
|
||||
test durable_disconnect_removes_last_temporary_room_without_shutdown_export ... ok
|
||||
test journal_failure_returns_no_routing_and_rolls_back_role_mutation ... ok
|
||||
test acknowledged_kick_and_room_destruction_cannot_resurrect_membership ... ok
|
||||
test dormant_deadline_stays_fixed_when_another_occupant_recovers_and_sends ... ok
|
||||
test temporary_membership_and_full_bounded_history_survive_ungraceful_loss ... ok
|
||||
|
||||
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
|
||||
|
||||
Running tests/muc_domain_bans.rs (target/debug/deps/muc_domain_bans-bf16a8c751b147a4)
|
||||
|
||||
running 2 tests
|
||||
test native_domain_bans_block_remote_users_but_allow_explicit_members ... ok
|
||||
test applying_domain_ban_removes_remote_occupants_and_blocks_rejoin ... ok
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
|
||||
|
||||
Running tests/muc_extensions.rs (target/debug/deps/muc_extensions-8aa1a3ac3d2930b2)
|
||||
|
||||
running 10 tests
|
||||
test disabled_optional_vcard_and_occupant_ids_are_not_advertised_or_emitted ... ok
|
||||
test broadcast_roles_hide_visitors_and_configuration_announces_transition ... ok
|
||||
test messages_cannot_spoof_room_identity_hats_or_occupant_identifiers ... ok
|
||||
test mam_all_rooms_presence_logging_public_queries_and_optional_totals ... ok
|
||||
test large_optional_history_does_not_prevent_checkpointing_membership ... ok
|
||||
test restricted_private_messages_allow_self_and_optional_moderators ... ok
|
||||
test offline_reserved_affiliates_follow_anonymity_and_roominfo_counts_nicknames ... ok
|
||||
test language_config_sections_discovery_and_native_affiliation_hats ... ok
|
||||
test recovery_claim_is_durable_and_unrelated_disconnect_keeps_temporary_claims ... ok
|
||||
test explicit_new_nickname_consumes_old_proof_and_pending_nicks_are_reserved ... ok
|
||||
|
||||
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
|
||||
|
||||
Running tests/muc_flow.rs (target/debug/deps/muc_flow-358a28db146dbe13)
|
||||
|
||||
running 18 tests
|
||||
test configured_lock_timeout_expires_without_owner_submission ... ok
|
||||
test iq_response_requires_expected_sender ... ok
|
||||
test multiple_resources_share_nick_and_private_delivery ... ok
|
||||
test join_lock_chat_leave ... ok
|
||||
test authorization_and_conflict ... ok
|
||||
test nick_invite_destroy_and_local_creation ... ok
|
||||
test occupant_id_is_stable_and_spoofed_ids_removed ... ok
|
||||
test preserves_unknown_binary_mixed_room_fields ... ok
|
||||
test restored_temporary_room_is_deleted_after_last_disconnect ... ok
|
||||
test native_muc_archive_type_and_anonymity_are_preserved ... ok
|
||||
test persistence_roundtrip ... ok
|
||||
test maintenance_expires_idle_archives_and_tombstones ... ok
|
||||
test persistent_destroy_creates_prosody_tombstone ... ok
|
||||
test switching_to_members_only_removes_unaffiliated_users ... ok
|
||||
test archive_history_and_permissions ... ok
|
||||
test room_vcard_owner_only_and_avatar_presence ... ok
|
||||
test visitor_voice_request_cannot_spoof_identity_or_grant_itself_voice ... ok
|
||||
test reserved_nickname_blocks_other_users ... ok
|
||||
|
||||
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
|
||||
|
||||
Running tests/muc_history.rs (target/debug/deps/muc_history-f67a7a69e0bfd957)
|
||||
|
||||
running 10 tests
|
||||
test capacity_changes_trim_and_zero_disables_history ... ok
|
||||
test only_direct_history_children_are_parsed ... ok
|
||||
test maxchars_counts_transmitted_utf8_bytes_including_address_and_delay ... ok
|
||||
test explicit_count_and_zero_limit ... ok
|
||||
test snapshot_restores_last_message_without_adding_another_delay ... ok
|
||||
test replay_carries_room_delay_and_recipient ... ok
|
||||
test ordinary_history_is_bounded_and_returns_oldest_first ... ok
|
||||
test subjects_and_hints_follow_native_history_eligibility ... ok
|
||||
test since_and_seconds_use_stricter_cutoff_inclusive ... ok
|
||||
test default_count_applies_only_without_valid_explicit_constraints ... ok
|
||||
|
||||
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/muc_native.rs (target/debug/deps/muc_native-f792fb43c05f2836)
|
||||
|
||||
running 1 test
|
||||
test actual_prosody_muc_mam_reads_rust_and_writes_native_archive ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/muc_optional_admin.rs (target/debug/deps/muc_optional_admin-cda5bde07d3a924e)
|
||||
|
||||
running 4 tests
|
||||
test default_registration_denies_unaffiliated_users ... ok
|
||||
test configured_tombstone_expiry_allows_immediate_recreation ... ok
|
||||
test optional_registration_grants_membership_and_enforces_reserved_nickname ... ok
|
||||
test administration_commands_recheck_authority_bind_sessions_and_update_live_rooms ... ok
|
||||
|
||||
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
|
||||
|
||||
Running tests/muc_protocol.rs (target/debug/deps/muc_protocol-5f42030bfe1d6522)
|
||||
|
||||
running 5 tests
|
||||
test ordinary_presence_extensions_survive_but_client_identity_is_filtered ... ok
|
||||
test subject_with_body_remains_an_ordinary_groupchat_message ... ok
|
||||
test probes_and_invalid_presence_cannot_join_or_rename ... ok
|
||||
test error_messages_kick_only_on_delivery_failure_and_presence_errors_kick ... ok
|
||||
test room_creation_policies_apply_only_to_new_rooms ... ok
|
||||
|
||||
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
|
||||
|
||||
Running tests/muc_recovery.rs (target/debug/deps/muc_recovery-226d89017c6b58d8)
|
||||
|
||||
running 7 tests
|
||||
test claim_is_one_time_and_does_not_change_untrusted_role ... ok
|
||||
test pending_snapshot_keeps_original_checkpoint_time ... ok
|
||||
test recovery_claim_requires_exact_authenticated_resource_and_expires ... ok
|
||||
test native_last_message_roundtrip_preserves_namespaces_and_rejects_other_rooms ... ok
|
||||
test native_checkpoint_roundtrip_filters_sensitive_extensions ... ok
|
||||
test oversized_status_never_prevents_membership_checkpoint ... ok
|
||||
test configuration_fingerprint_is_independent_of_lua_table_iteration_order ... ok
|
||||
|
||||
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
|
||||
|
||||
Running tests/muc_restart_state.rs (target/debug/deps/muc_restart_state-5cb216a0bea9ced1)
|
||||
|
||||
running 3 tests
|
||||
test unrelated_disconnect_does_not_delete_dormant_temporary_room ... ok
|
||||
test changed_configuration_invalidates_auto_recovery_without_disclosing_password ... ok
|
||||
test repeated_restart_keeps_dormant_claim_and_deadline_without_ghost_routing ... ok
|
||||
|
||||
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
|
||||
|
||||
Running tests/muc_state_native.rs (target/debug/deps/muc_state_native-e120e43e0108ea7f)
|
||||
|
||||
running 1 test
|
||||
test actual_prosody_restores_rust_checkpoint_and_rust_reads_native_freeze ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/pep_client_interop.rs (target/debug/deps/pep_client_interop-1553fc66a4e090e1)
|
||||
|
||||
running 7 tests
|
||||
test native_component_notification_type_applies_to_events_and_subscription_replay ... ok
|
||||
test missing_own_omemo_bundle_returns_recoverable_item_not_found ... ok
|
||||
test publish_options_match_implicit_notification_defaults_on_old_nodes ... ok
|
||||
test migrated_native_configuration_can_roundtrip_access_model_edit ... ok
|
||||
test avatar_and_omemo_publish_options_support_readback_and_caps_notifications ... ok
|
||||
test native_itemreply_controls_publisher_visibility_in_reads_and_replays ... ok
|
||||
test native_open_and_subscriber_publish_models_preserve_outcast_and_blocking_checks ... ok
|
||||
|
||||
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.24s
|
||||
|
||||
Running tests/pep_diagnostics.rs (target/debug/deps/pep_diagnostics-98b92a168aa66315)
|
||||
|
||||
running 6 tests
|
||||
test nonempty_xml_can_be_an_empty_android_device_list ... ok
|
||||
test avatar_classification_does_not_inspect_or_expose_avatar_data ... ok
|
||||
test arbitrary_node_payload_and_wrong_namespace_do_not_leak ... ok
|
||||
test only_first_direct_item_is_parsed_and_protocols_are_distinct ... ok
|
||||
test payloadless_notification_is_visible_without_identifiers ... ok
|
||||
test signed_integer_limits_and_duplicates_match_java_parser ... ok
|
||||
|
||||
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/pep_discovery.rs (target/debug/deps/pep_discovery-3e3fb8c04ba0a062)
|
||||
|
||||
running 4 tests
|
||||
test standalone_component_does_not_claim_pep_automatic_subscriptions ... ok
|
||||
test account_disco_advertises_pep_without_requiring_a_node ... ok
|
||||
test account_discovery_does_not_intercept_server_or_resource_caps ... ok
|
||||
test node_discovery_keeps_node_identity_errors_and_access_checks ... ok
|
||||
|
||||
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s
|
||||
|
||||
Running tests/pep_item_order.rs (target/debug/deps/pep_item_order-77f6b485fd7811b4)
|
||||
|
||||
running 2 tests
|
||||
test native_prosody_orders_retained_pep_items_newest_first ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
|
||||
test first_pep_item_is_current_device_list_even_with_retained_old_items ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.10s
|
||||
|
||||
Running tests/preauth.rs (target/debug/deps/preauth-4dd44275632445e0)
|
||||
|
||||
running 10 tests
|
||||
test duplicate_or_wrong_namespace_tokens_do_not_qualify ... ok
|
||||
test standard_adhoc_command_returns_contact_uri ... ok
|
||||
test unknown_tokens_do_not_create_invite_stores_and_disabled_accounts_cannot_issue ... ok
|
||||
test prosody_numeric_expiry_is_consumed_and_other_entries_preserved ... ok
|
||||
test concurrent_redemption_has_exactly_one_winner ... ok
|
||||
test unknown_client_token_is_preserved_on_remote_and_pending_delivery ... ok
|
||||
test identity_scope_does_not_burn_on_wrong_sender_or_leak_to_prosody ... ok
|
||||
test invitations_expire_are_single_use_and_owner_scoped ... ok
|
||||
test valid_token_autoapproves_and_sends_reciprocal_request ... ok
|
||||
test local_invitation_updates_both_rosters_without_granting_reverse_subscription ... ok
|
||||
|
||||
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
|
||||
|
||||
Running tests/preauth_native.rs (target/debug/deps/preauth_native-36f66c5afb8f3b7f)
|
||||
|
||||
running 1 test
|
||||
test native_prosody_invitation_tokens_roundtrip ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/pubsub_native.rs (target/debug/deps/pubsub_native-5bfe7997d9ab4681)
|
||||
|
||||
running 1 test
|
||||
test native_prosody_pep_reads_rust_and_rust_reads_native ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/pubsub_persistence.rs (target/debug/deps/pubsub_persistence-96d57930ef04b71a)
|
||||
|
||||
running 18 tests
|
||||
test modern_bookmarks_notify_legacy_capability ... ok
|
||||
test native_string_max_items_satisfies_numeric_publish_precondition ... ok
|
||||
test configurable_limits_component_creation_and_publisher ... ok
|
||||
test configure_requires_owner ... ok
|
||||
test capability_discovery_replays_and_filters_notifications ... ok
|
||||
test nonpersistent_items_stay_in_memory_and_can_be_made_persistent ... ok
|
||||
test open_model_and_notifications ... ok
|
||||
test reads_prosody_legacy_fixture ... ok
|
||||
test full_jid_block_applies_to_reads_subscription_replays_and_events ... ok
|
||||
test pep_persists_namespaces_and_enforces_owner ... ok
|
||||
test publish_options_conflict_is_recoverable_and_preserves_item ... ok
|
||||
test remote_unavailable_is_scoped_to_addressed_owner_and_cancels_pending ... ok
|
||||
test roster_presence_access_and_foreign_subscription_denied ... ok
|
||||
test remote_caps_replay_and_open_node_privacy_revoke ... ok
|
||||
test subscription_options_disable_events_and_require_subscriber ... ok
|
||||
test payload_policy_applies_to_subscription_and_caps_replays ... ok
|
||||
test native_notification_fields_and_retract_policy_are_independent ... ok
|
||||
test component_default_retention_matches_prosody_twenty_items ... ok
|
||||
|
||||
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.24s
|
||||
|
||||
Running tests/push.rs (target/debug/deps/push-e74594c0e96b1321)
|
||||
|
||||
running 7 tests
|
||||
test native_push_body_lookup_excludes_extensions ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
|
||||
test foreign_account_cannot_register ... ok
|
||||
test queue_coalesces_and_importance_matches_native ... ok
|
||||
test push_summary_never_copies_foreign_namespace_body ... ok
|
||||
test reenable_cancels_old_errors_and_failure_counts_are_per_account ... ok
|
||||
test stable_client_registration_and_targeted_notification ... ok
|
||||
test registrations_notifications_and_authenticated_errors ... ok
|
||||
|
||||
test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.12s
|
||||
|
||||
Running tests/reference_prosody.rs (target/debug/deps/reference_prosody-fe62c7cdfcb258f2)
|
||||
|
||||
running 1 test
|
||||
test real_prosody_storage_and_auth_roundtrip ... ignored, requires native supplied Prosody source; run scripts/prosody_compat/verify.sh
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/resource_sessions.rs (target/debug/deps/resource_sessions-c00bb0104e05dfa7)
|
||||
|
||||
running 7 tests
|
||||
test increment_skips_occupied_suffixes_and_preserves_sessions ... ok
|
||||
test invalid_resume_owner_or_ack_does_not_evict_active_session ... ok
|
||||
test kick_new_leaves_existing_session_unchanged ... ok
|
||||
test replacement_revokes_hibernating_resume_and_survives_old_cleanup ... ok
|
||||
test random_conflicts_get_distinct_resources_for_same_owner ... ok
|
||||
test concurrent_default_binds_replace_atomically ... ok
|
||||
test active_resume_preserves_state_and_replaces_transport_notifier ... ok
|
||||
|
||||
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
|
||||
|
||||
Running tests/sasl_accounts.rs (target/debug/deps/sasl_accounts-c2c6690c56d78954)
|
||||
|
||||
running 1 test
|
||||
test disabled_legacy_and_unicode_accounts ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.63s
|
||||
|
||||
Running tests/sm_durable_slots.rs (target/debug/deps/sm_durable_slots-cf591ebfb421691b)
|
||||
|
||||
running 11 tests
|
||||
test corrupt_committed_slot_fails_recovery_closed ... ok
|
||||
test durable_tokens_and_private_pending_output_have_owner_only_permissions ... ok
|
||||
test recovery_discards_owned_crash_temps_and_keeps_last_committed_slot ... ok
|
||||
test session_reference_does_not_keep_store_locked_or_allow_unpersisted_writes ... ok
|
||||
test disconnected_deadline_is_fixed_across_recovery ... ok
|
||||
test durable_counters_wrap_without_accepting_stale_acknowledgements ... ok
|
||||
test shutdown_freeze_retires_old_transport_before_cleanup ... ok
|
||||
test independent_hubs_with_identical_connection_names_keep_their_own_slots ... ok
|
||||
test restart_restores_owner_presence_and_only_unacknowledged_output ... ok
|
||||
test graceful_close_conflict_account_purge_and_expiration_remove_tokens ... ok
|
||||
test store_has_one_process_owner_and_enforces_output_byte_bound ... ok
|
||||
|
||||
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.26s
|
||||
|
||||
Running tests/storage_accounts.rs (target/debug/deps/storage_accounts-1d75e90b4e9212f9)
|
||||
|
||||
running 20 tests
|
||||
test account_purge_refuses_symlink_store_without_touching_other_files ... ok
|
||||
test broken_roster_is_not_overwritten ... ok
|
||||
test inbound_subscription_requires_pending_to_grant_to ... ok
|
||||
test blocklist_enforcement_api ... ok
|
||||
test domain_roster_and_block_jid_normalization ... ok
|
||||
test malformed_roster_iq_never_mutates ... ok
|
||||
test malformed_legacy_vcard_and_private_bookmarks_rejected ... ok
|
||||
test private_and_vcard_authorization ... ok
|
||||
test privacy_migration_only_default_deny_jids ... ok
|
||||
test reads_timestamp_blocklist_and_keeps_metadata ... ok
|
||||
test roster_empty_name_and_foreign_group_ignored ... ok
|
||||
test roster_preserves_subscription_and_metadata ... ok
|
||||
test purge_account_exact_files_preserves_others_and_extras ... ok
|
||||
test subscription_handshake_and_cancel ... ok
|
||||
test legacy_bookmarks_respect_host_limit_and_read_over_256 ... ok
|
||||
test bookmarks_private_pep_bidirectional_and_private ... ok
|
||||
test legacy_migration_clears_source_after_success ... ok
|
||||
test vcard_pep_conversion_and_avatar ... ok
|
||||
test password_change_after_purge_never_recreates_account ... ok
|
||||
test creation_and_purge_are_serialized ... ok
|
||||
|
||||
test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.36s
|
||||
|
||||
Running tests/storage_compat.rs (target/debug/deps/storage_compat-147836efd1c208ca)
|
||||
|
||||
running 6 tests
|
||||
test empty_namespace_preserved ... ok
|
||||
test mixed_binary_roundtrip ... ok
|
||||
test stanza_roundtrip ... ok
|
||||
test path_and_atomic_update ... ok
|
||||
test rejects_cycles_code_and_bad_files ... ok
|
||||
test concurrent_updates_do_not_lose_entries ... ok
|
||||
|
||||
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
|
||||
|
||||
Running tests/tls_binding.rs (target/debug/deps/tls_binding-5e699c7900a4d099)
|
||||
|
||||
running 4 tests
|
||||
test malformed_der_and_pss_parameters_fail_closed ... ok
|
||||
test unknown_and_eddsa_algorithms_fail_closed ... ok
|
||||
test ecdsa_uses_certificate_signature_hash ... ok
|
||||
test rsa_signature_digest_and_weak_hash_upgrade ... ok
|
||||
|
||||
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s
|
||||
|
||||
Running tests/tls_plus_network.rs (target/debug/deps/tls_plus_network-b82a1b9340e268e4)
|
||||
|
||||
running 2 tests
|
||||
test disabling_plus_hides_bindings_and_accepts_scram_y_with_mechanisms_only_ssdp ... ok
|
||||
test starttls_scram_plus_binds_real_tls_and_authenticates_ssdp_sasl1_and_sasl2 ... ok
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.37s
|
||||
|
||||
Doc-tests rust_xmpp
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Finished `dev` profile [unoptimized] target(s) in 0.10s
|
||||
Finished `release` profile [optimized] target(s) in 0.11s
|
||||
PASS: integrated TCP roster/subscriptions, client IQ routing, CSI buffering/keepalive flush, MAM, blocking, OMEMO PEP, MUC, offline replay and SM resumption
|
||||
PASS: graceful SIGTERM shutdown
|
||||
PASS: independent SCRAM-SHA-1 over TLS, SASL1/SASL2, SSDP verification, server signatures and wrong-password rejection
|
||||
PASS: independent SCRAM-SHA-256 over TLS, SASL1/SASL2, SSDP verification, server signatures and wrong-password rejection
|
||||
PASS: Android TLS1.2 selector, TLS1.2/1.3 SCRAM SHA1/256 PLUS, SASL1/2, SSDP on/off and invalid binding rejection
|
||||
PASS: JSON file logging, TLS/auth/bind correlation, rejection reasons, secret redaction and SIGHUP rotation
|
||||
PASS: 20 TLS1.2/1.3 SASL1/2 cases: prepared PLAIN identities and canonical binds, invalid authz rejection, SCRAM abort/retry and active exchange restart
|
||||
PASS: WSS SCRAM-SHA-256 SASL1/SASL2 with GS2 y, absent CB/PLUS, SSDP without CB separator, server signatures, bind and ping
|
||||
PASS: HTTPS-only configured interfaces (127.0.0.1,::1), verified TLS, reproduced original Origin 403 then allowed Converse Origin 101, xmpp subprotocol, both WS paths, RFC 7395 open/features/close and private HTTP diagnostics
|
||||
PASS: custom WS/BOSH routes, default-route 404, exact Origin policy, verified TLS proxy rewrite, trusted HTTPS metadata, untrusted/spoofed metadata denial, IPv6 when available, authenticated WS/BOSH bind/ping in legacy and RFC7239 modes and cached BOSH RID retry
|
||||
PASS: XEP-0156 XRD/JRD parity, CORS, safe methods, ignored JSONP, host checks, secure URL preference, custom mounts/public prefixes, module and advertisement gates, HTTP fallback and verified WSS framing
|
||||
PASS: SASL2 Bind2 TLS flow, inline SM/CSI/Carbons, private random resource, malformed requests, authentication failure and fallback
|
||||
PASS: bind2-disabled, SSDP=False, SCRAM-SHA-1/PLAIN success and wrong-password rejection
|
||||
PASS: bind2-disabled, SSDP=True, SCRAM-SHA-1/PLAIN success and wrong-password rejection
|
||||
PASS: bind2-enabled, SSDP=False, SCRAM-SHA-1/PLAIN success and wrong-password rejection
|
||||
PASS: bind2-enabled, SSDP=True, SCRAM-SHA-1/PLAIN success and wrong-password rejection
|
||||
PASS: sm-disabled, SSDP=False, SCRAM-SHA-1/PLAIN success and wrong-password rejection
|
||||
PASS: sm-disabled, SSDP=True, SCRAM-SHA-1/PLAIN success and wrong-password rejection
|
||||
PASS: enabled-False-registration-False
|
||||
PASS: enabled-False-registration-True
|
||||
PASS: enabled-True-registration-False
|
||||
PASS: enabled-True-registration-True
|
||||
PASS: Gajim domain/account discovery, both label catalogs, authenticated password changes, IBR registration gates and fresh logins
|
||||
PASS: SASL2 inline SM active/EOF resume, authenticated owner/id/h checks, replay ordering, full JID, Bind2 fallback and malformed requests
|
||||
PASS: TLS account/domain discovery with PEP and push enabled/disabled; real push enable, publish options, per-node and service-wide disable
|
||||
PASS: TLS Monocles/Conversations PEP first-run, no-to publishing, owner config recovery, migrated metadata, device item selectors and restart persistence
|
||||
PASS: Android first-item device-list parsing sees latest publish and replacement before and after restart
|
||||
PASS: STARTTLS PEP replay/live diagnostics, legacy/OMEMO2, payloadless notifications with intact IQ payloads, and private values excluded
|
||||
PASS: TLS invitation command discovery, preapproval, single-use mutual roster, replay denial, unknown preauth forwarding/offline pending, account isolation
|
||||
PASS: STARTTLS MUC SIGTERM restart restores live temporary/persistent rooms, exact full-JID membership, roles, presence and groupchat routing; rejects automatic cross-resource/account claims and nickname theft
|
||||
PASS: recovery-history
|
||||
PASS: leave
|
||||
PASS: kick
|
||||
PASS: demotion
|
||||
PASS: password
|
||||
PASS: configuration
|
||||
PASS: restart-grace
|
||||
PASS: custom-grace
|
||||
PASS: unknown-module
|
||||
PASS: actual STARTTLS SIGKILL MUC recovery, history, acknowledged barriers and fixed grace
|
||||
PASS: TLS legacy/SASL2 durable SM after SIGKILL/SIGTERM, counters, ack pruning, at-least-once pending replay, owner/token/h rejection, transport/resource conflict, MUC activation/offline fanout, grace expiry and account deletion/recreation revocation
|
||||
PASS: TLS conflict_resolve=default, live and hibernating SM resources
|
||||
PASS: TLS conflict_resolve=kick_old, live and hibernating SM resources
|
||||
PASS: TLS conflict_resolve=kick_new, live and hibernating SM resources
|
||||
PASS: TLS conflict_resolve=increment, live and hibernating SM resources
|
||||
PASS: TLS conflict_resolve=random, live and hibernating SM resources
|
||||
PASS: integrated WSS + HTTPS BOSH SASL/bind, CSI, inherited BOSH namespaces, roster, bidirectional chat, upload IQ/PUT/GET/reuse, graceful SM close and same-resource relogin under kick_new
|
||||
PASS: integrated WSS + HTTPS BOSH SASL/bind, CSI, inherited BOSH namespaces, roster, bidirectional chat, upload IQ/PUT/GET/reuse, graceful SM close and same-resource relogin under kick_new
|
||||
PASS: configured upload HTTP path with $host through integrated listener
|
||||
PASS: multiple upload hosts/shared-host paths, discovery, quotas, JWT isolation, persistence, transport composition and conflict validation
|
||||
PASS upload S3 protocol fixture: SigV4, multipart, abort, range, restart, expiry and backend isolation
|
||||
PASS upload migration: local -> S3 -> S3 -> local, hashes, original URLs/JWT/HEAD, lock, dry-run, fail-closed resume
|
||||
{
|
||||
"total_load_wall_seconds": 6.57,
|
||||
"requested_resources": 500,
|
||||
"authenticated_bound_resources": 500,
|
||||
"connection_setup_concurrency": 50,
|
||||
"setup_wall_seconds": 0.617,
|
||||
"setup_latency": {
|
||||
"samples": 500,
|
||||
"median_ms": 57.355,
|
||||
"p95_ms": 75.79,
|
||||
"max_ms": 93.594
|
||||
},
|
||||
"ping_rounds": 10,
|
||||
"ping_round_trip": {
|
||||
"samples": 5000,
|
||||
"median_ms": 12.184,
|
||||
"p95_ms": 25.508,
|
||||
"max_ms": 28.647
|
||||
},
|
||||
"accounts": 100,
|
||||
"message_rounds": 10,
|
||||
"messages_sent": 5000,
|
||||
"messages_received": 5000,
|
||||
"message_batch_seconds": 3.668,
|
||||
"metrics_before": {
|
||||
"rss_kib": 8532,
|
||||
"cpu_seconds": 0.0,
|
||||
"fd_count": 11
|
||||
},
|
||||
"metrics_connected": {
|
||||
"rss_kib": 27360,
|
||||
"cpu_seconds": 1.06,
|
||||
"fd_count": 511
|
||||
},
|
||||
"metrics_after_workload": {
|
||||
"rss_kib": 28192,
|
||||
"cpu_seconds": 8.11,
|
||||
"fd_count": 511
|
||||
},
|
||||
"metrics_two_seconds_after_disconnect": {
|
||||
"rss_kib": 28192,
|
||||
"cpu_seconds": 8.13,
|
||||
"fd_count": 11
|
||||
},
|
||||
"sampled_peak_rss_kib": 28192,
|
||||
"fd_delta_after_disconnect": 0,
|
||||
"failures": [],
|
||||
"scope": "One loopback host, 100 accounts with distinct resources, TLS+PLAIN, ping and directed messages; not a production benchmark or proof of no memory leaks.",
|
||||
"mam": {
|
||||
"archive_files": 100,
|
||||
"records": 10000,
|
||||
"expected_records": 10000
|
||||
}
|
||||
}
|
||||
|
||||
# Repeated MUC+SM load after correcting test readiness timeout (same release binary)
|
||||
MUC+SM load: connected (5.193s)
|
||||
MUC+SM load: joined (51.901s)
|
||||
MUC+SM load: fanout_verified (62.762s)
|
||||
MUC+SM load: before_sigkill (67.577s)
|
||||
{
|
||||
"clients": 500,
|
||||
"accounts": 100,
|
||||
"rooms": 10,
|
||||
"message_rounds": 3,
|
||||
"concurrency": 20,
|
||||
"binary": "/tmp/xmpp-durable-muc-load-g0crm4ik/rust-xmpp",
|
||||
"binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
|
||||
"before": {
|
||||
"rss_kib": 9696,
|
||||
"cpu_seconds": 0.0,
|
||||
"fd_count": 12
|
||||
},
|
||||
"connect_seconds": 5.193,
|
||||
"connect_latency": {
|
||||
"samples": 500,
|
||||
"median_ms": 204.391,
|
||||
"p95_ms": 270.035,
|
||||
"max_ms": 332.503
|
||||
},
|
||||
"phase": "completed",
|
||||
"status": "pass",
|
||||
"elapsed_seconds": 67.577,
|
||||
"join_seconds": 46.703,
|
||||
"joined": {
|
||||
"rss_kib": 42248,
|
||||
"cpu_seconds": 17.77,
|
||||
"fd_count": 513
|
||||
},
|
||||
"join_presence_deliveries": 25000,
|
||||
"slots_joined": {
|
||||
"count": 500,
|
||||
"bytes": 309790
|
||||
},
|
||||
"message_seconds": 10.861,
|
||||
"room_messages_sent": 30,
|
||||
"room_messages_delivered": 1500,
|
||||
"before_kill": {
|
||||
"rss_kib": 42128,
|
||||
"cpu_seconds": 23.12,
|
||||
"fd_count": 514
|
||||
},
|
||||
"slots_before_kill": {
|
||||
"count": 500,
|
||||
"bytes": 514030
|
||||
},
|
||||
"restart_seconds": 10.559,
|
||||
"resumed_resources": 10,
|
||||
"replayed_pending_room_messages": 10,
|
||||
"membership_groupchat_deliveries_after_resume": 10,
|
||||
"after_resume": {
|
||||
"rss_kib": 21168,
|
||||
"cpu_seconds": 5.03,
|
||||
"fd_count": 23
|
||||
},
|
||||
"slots_after_resume": {
|
||||
"count": 500,
|
||||
"bytes": 671070
|
||||
},
|
||||
"total_seconds": 78.857,
|
||||
"peak_rss_kib": 42252
|
||||
}
|
||||
|
||||
# Native reference checks
|
||||
Finished `test` profile [unoptimized] target(s) in 0.09s
|
||||
Running tests/csi_native.rs (target/debug/deps/csi_native-7a0bbbead7a19df9)
|
||||
|
||||
running 1 test
|
||||
test native_prosody_csi_importance_matches ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
|
||||
|
||||
Running tests/muc_native.rs (target/debug/deps/muc_native-f792fb43c05f2836)
|
||||
|
||||
running 1 test
|
||||
test actual_prosody_muc_mam_reads_rust_and_writes_native_archive ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s
|
||||
|
||||
Running tests/muc_state_native.rs (target/debug/deps/muc_state_native-e120e43e0108ea7f)
|
||||
|
||||
running 1 test
|
||||
test actual_prosody_restores_rust_checkpoint_and_rust_reads_native_freeze ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
|
||||
|
||||
Running tests/pep_item_order.rs (target/debug/deps/pep_item_order-77f6b485fd7811b4)
|
||||
|
||||
running 1 test
|
||||
test native_prosody_orders_retained_pep_items_newest_first ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.13s
|
||||
|
||||
Running tests/preauth_native.rs (target/debug/deps/preauth_native-36f66c5afb8f3b7f)
|
||||
|
||||
running 1 test
|
||||
test native_prosody_invitation_tokens_roundtrip ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s
|
||||
|
||||
Running tests/pubsub_native.rs (target/debug/deps/pubsub_native-5bfe7997d9ab4681)
|
||||
|
||||
running 1 test
|
||||
test native_prosody_pep_reads_rust_and_rust_reads_native ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s
|
||||
|
||||
Running tests/push.rs (target/debug/deps/push-e74594c0e96b1321)
|
||||
|
||||
running 1 test
|
||||
test native_push_body_lookup_excludes_extensions ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.01s
|
||||
|
||||
Running tests/reference_prosody.rs (target/debug/deps/reference_prosody-fe62c7cdfcb258f2)
|
||||
|
||||
running 1 test
|
||||
PROSODY_NATIVE_ROUNDTRIP_OK SHA-1
|
||||
PROSODY_NATIVE_FINAL_OK SHA-1
|
||||
PROSODY_NATIVE_ROUNDTRIP_OK SHA-256
|
||||
PROSODY_NATIVE_FINAL_OK SHA-256
|
||||
test real_prosody_storage_and_auth_roundtrip ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.03s
|
||||
|
||||
Finished `test` profile [unoptimized] target(s) in 0.09s
|
||||
Running tests/federation_tls.rs (target/debug/deps/federation_tls-6dc128df24b929d8)
|
||||
|
||||
running 1 test
|
||||
test native_prosody_tls_external_bidirectional ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.88s
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"binary_sha256": "c57757476f2019c60ac877e4a66364b17283218e3cc244ae3f29cde1d3be2977",
|
||||
"scope": "loopback STARTTLS; isolated fixture; no production latency claim",
|
||||
"cases": [
|
||||
{
|
||||
"mam_enabled": false,
|
||||
"sm_enabled": true,
|
||||
"rounds": 3,
|
||||
"seeded_muc_archive_records": 0,
|
||||
"active": {
|
||||
"direct_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 2.188561000025402,
|
||||
"p95_ms": 2.3107499999923675,
|
||||
"max_ms": 2.3107499999923675,
|
||||
"samples_ms": [
|
||||
2.0956669999350197,
|
||||
2.188561000025402,
|
||||
2.3107499999923675
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 3,
|
||||
"median_ms": 4.38233800002763,
|
||||
"p95_ms": 4.56658600000992,
|
||||
"max_ms": 4.56658600000992,
|
||||
"samples_ms": [
|
||||
4.381051999985175,
|
||||
4.38233800002763,
|
||||
4.56658600000992
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 3,
|
||||
"median_ms": 3.617779000023802,
|
||||
"p95_ms": 3.788068999938332,
|
||||
"max_ms": 3.788068999938332,
|
||||
"samples_ms": [
|
||||
3.264654999952654,
|
||||
3.617779000023802,
|
||||
3.788068999938332
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 3.8124090000337674,
|
||||
"p95_ms": 3.8366489999361875,
|
||||
"max_ms": 3.8366489999361875,
|
||||
"samples_ms": [
|
||||
3.3308939999869835,
|
||||
3.8124090000337674,
|
||||
3.8366489999361875
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"direct_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 2.189512999962062,
|
||||
"p95_ms": 2.2236539999767047,
|
||||
"max_ms": 2.2236539999767047,
|
||||
"samples_ms": [
|
||||
2.1697740000945487,
|
||||
2.189512999962062,
|
||||
2.2236539999767047
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 3,
|
||||
"median_ms": 4.169533999970554,
|
||||
"p95_ms": 4.451736999953937,
|
||||
"max_ms": 4.451736999953937,
|
||||
"samples_ms": [
|
||||
4.0612270000792705,
|
||||
4.169533999970554,
|
||||
4.451736999953937
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 3,
|
||||
"median_ms": 3.311576999976751,
|
||||
"p95_ms": 4.169272000012825,
|
||||
"max_ms": 4.169272000012825,
|
||||
"samples_ms": [
|
||||
3.2493040000645124,
|
||||
3.311576999976751,
|
||||
4.169272000012825
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 3.372077999983958,
|
||||
"p95_ms": 4.188992999957009,
|
||||
"max_ms": 4.188992999957009,
|
||||
"samples_ms": [
|
||||
3.315083000074992,
|
||||
3.372077999983958,
|
||||
4.188992999957009
|
||||
]
|
||||
}
|
||||
},
|
||||
"urgent_flush": {
|
||||
"samples": 1,
|
||||
"median_ms": 3.6135210000338702,
|
||||
"p95_ms": 3.6135210000338702,
|
||||
"max_ms": 3.6135210000338702,
|
||||
"samples_ms": [
|
||||
3.6135210000338702
|
||||
]
|
||||
},
|
||||
"headline_durably_buffered": true,
|
||||
"activation_drained_queue": true,
|
||||
"personal_archive_records": 0,
|
||||
"muc_archive_records": 0,
|
||||
"wire_body_deliveries": 27,
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"mam_enabled": true,
|
||||
"sm_enabled": true,
|
||||
"rounds": 3,
|
||||
"seeded_muc_archive_records": 100000,
|
||||
"active": {
|
||||
"direct_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 4.533615000013924,
|
||||
"p95_ms": 4.635108000002219,
|
||||
"max_ms": 4.635108000002219,
|
||||
"samples_ms": [
|
||||
3.9327949999687917,
|
||||
4.533615000013924,
|
||||
4.635108000002219
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 3,
|
||||
"median_ms": 8.584756000004745,
|
||||
"p95_ms": 9.454648999962956,
|
||||
"max_ms": 9.454648999962956,
|
||||
"samples_ms": [
|
||||
8.391761999973824,
|
||||
8.584756000004745,
|
||||
9.454648999962956
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 3,
|
||||
"median_ms": 4.660852999904819,
|
||||
"p95_ms": 14.45046399999228,
|
||||
"max_ms": 14.45046399999228,
|
||||
"samples_ms": [
|
||||
4.372101999933875,
|
||||
4.660852999904819,
|
||||
14.45046399999228
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 4.680227999983799,
|
||||
"p95_ms": 14.532127000052242,
|
||||
"max_ms": 14.532127000052242,
|
||||
"samples_ms": [
|
||||
4.40479399992455,
|
||||
4.680227999983799,
|
||||
14.532127000052242
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"direct_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 5.005547999985538,
|
||||
"p95_ms": 5.457555000020875,
|
||||
"max_ms": 5.457555000020875,
|
||||
"samples_ms": [
|
||||
4.749896000021181,
|
||||
5.005547999985538,
|
||||
5.457555000020875
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 3,
|
||||
"median_ms": 9.33329400004368,
|
||||
"p95_ms": 9.77378499999304,
|
||||
"max_ms": 9.77378499999304,
|
||||
"samples_ms": [
|
||||
8.875206999960028,
|
||||
9.33329400004368,
|
||||
9.77378499999304
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 3,
|
||||
"median_ms": 4.230191000033301,
|
||||
"p95_ms": 5.015898000010566,
|
||||
"max_ms": 5.015898000010566,
|
||||
"samples_ms": [
|
||||
4.2100000000573345,
|
||||
4.230191000033301,
|
||||
5.015898000010566
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 4.284876000042459,
|
||||
"p95_ms": 5.071866999969643,
|
||||
"max_ms": 5.071866999969643,
|
||||
"samples_ms": [
|
||||
4.257245000076182,
|
||||
4.284876000042459,
|
||||
5.071866999969643
|
||||
]
|
||||
}
|
||||
},
|
||||
"urgent_flush": {
|
||||
"samples": 1,
|
||||
"median_ms": 5.098277999991296,
|
||||
"p95_ms": 5.098277999991296,
|
||||
"max_ms": 5.098277999991296,
|
||||
"samples_ms": [
|
||||
5.098277999991296
|
||||
]
|
||||
},
|
||||
"headline_durably_buffered": true,
|
||||
"activation_drained_queue": true,
|
||||
"personal_archive_records": 26,
|
||||
"muc_archive_records": 100006,
|
||||
"wire_body_deliveries": 27,
|
||||
"status": "pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
|
||||
"scope": "loopback STARTTLS; isolated fixture; no production latency claim",
|
||||
"cases": [
|
||||
{
|
||||
"mam_enabled": false,
|
||||
"sm_enabled": true,
|
||||
"rounds": 3,
|
||||
"seeded_muc_archive_records": 0,
|
||||
"active": {
|
||||
"direct_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 4.910238999968897,
|
||||
"p95_ms": 5.052589999991142,
|
||||
"max_ms": 5.052589999991142,
|
||||
"samples_ms": [
|
||||
4.849958000022525,
|
||||
4.910238999968897,
|
||||
5.052589999991142
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 3,
|
||||
"median_ms": 9.99482099996385,
|
||||
"p95_ms": 10.185542000044734,
|
||||
"max_ms": 10.185542000044734,
|
||||
"samples_ms": [
|
||||
9.438553999984833,
|
||||
9.99482099996385,
|
||||
10.185542000044734
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 3,
|
||||
"median_ms": 7.185058999993998,
|
||||
"p95_ms": 7.319806000054996,
|
||||
"max_ms": 7.319806000054996,
|
||||
"samples_ms": [
|
||||
5.824679000056676,
|
||||
7.185058999993998,
|
||||
7.319806000054996
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 7.9024159999789845,
|
||||
"p95_ms": 8.338388999959534,
|
||||
"max_ms": 8.338388999959534,
|
||||
"samples_ms": [
|
||||
6.551824999974087,
|
||||
7.9024159999789845,
|
||||
8.338388999959534
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"direct_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 6.445996999900672,
|
||||
"p95_ms": 7.172110000055909,
|
||||
"max_ms": 7.172110000055909,
|
||||
"samples_ms": [
|
||||
4.87915000007888,
|
||||
6.445996999900672,
|
||||
7.172110000055909
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 3,
|
||||
"median_ms": 10.659646999897632,
|
||||
"p95_ms": 12.850071000002572,
|
||||
"max_ms": 12.850071000002572,
|
||||
"samples_ms": [
|
||||
9.232021000002533,
|
||||
10.659646999897632,
|
||||
12.850071000002572
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 3,
|
||||
"median_ms": 9.392836000074567,
|
||||
"p95_ms": 14.882106999948519,
|
||||
"max_ms": 14.882106999948519,
|
||||
"samples_ms": [
|
||||
5.741209000007075,
|
||||
9.392836000074567,
|
||||
14.882106999948519
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 10.40252400002828,
|
||||
"p95_ms": 16.07552799998757,
|
||||
"max_ms": 16.07552799998757,
|
||||
"samples_ms": [
|
||||
6.522893999999724,
|
||||
10.40252400002828,
|
||||
16.07552799998757
|
||||
]
|
||||
}
|
||||
},
|
||||
"urgent_flush": {
|
||||
"samples": 1,
|
||||
"median_ms": 6.09094300000379,
|
||||
"p95_ms": 6.09094300000379,
|
||||
"max_ms": 6.09094300000379,
|
||||
"samples_ms": [
|
||||
6.09094300000379
|
||||
]
|
||||
},
|
||||
"headline_durably_buffered": true,
|
||||
"activation_drained_queue": true,
|
||||
"personal_archive_records": 0,
|
||||
"muc_archive_records": 0,
|
||||
"wire_body_deliveries": 27,
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"mam_enabled": true,
|
||||
"sm_enabled": true,
|
||||
"rounds": 3,
|
||||
"seeded_muc_archive_records": 100000,
|
||||
"active": {
|
||||
"direct_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 8.917705999920145,
|
||||
"p95_ms": 10.107052999956068,
|
||||
"max_ms": 10.107052999956068,
|
||||
"samples_ms": [
|
||||
8.420038000053864,
|
||||
8.917705999920145,
|
||||
10.107052999956068
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 3,
|
||||
"median_ms": 16.42384999991009,
|
||||
"p95_ms": 17.16581000005135,
|
||||
"max_ms": 17.16581000005135,
|
||||
"samples_ms": [
|
||||
16.391191000025174,
|
||||
16.42384999991009,
|
||||
17.16581000005135
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 3,
|
||||
"median_ms": 2517.3544539999284,
|
||||
"p95_ms": 2520.2535930000067,
|
||||
"max_ms": 2520.2535930000067,
|
||||
"samples_ms": [
|
||||
2501.2779300000147,
|
||||
2517.3544539999284,
|
||||
2520.2535930000067
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 2517.9402029999665,
|
||||
"p95_ms": 2520.940784000004,
|
||||
"max_ms": 2520.940784000004,
|
||||
"samples_ms": [
|
||||
2502.2604349999256,
|
||||
2517.9402029999665,
|
||||
2520.940784000004
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"direct_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 24.452612999994017,
|
||||
"p95_ms": 26.775254999961362,
|
||||
"max_ms": 26.775254999961362,
|
||||
"samples_ms": [
|
||||
8.767078000005313,
|
||||
24.452612999994017,
|
||||
26.775254999961362
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 3,
|
||||
"median_ms": 36.83773900002052,
|
||||
"p95_ms": 50.86929899994175,
|
||||
"max_ms": 50.86929899994175,
|
||||
"samples_ms": [
|
||||
17.80327500000567,
|
||||
36.83773900002052,
|
||||
50.86929899994175
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 3,
|
||||
"median_ms": 3855.8298680000007,
|
||||
"p95_ms": 4150.020520999988,
|
||||
"max_ms": 4150.020520999988,
|
||||
"samples_ms": [
|
||||
2901.40261199997,
|
||||
3855.8298680000007,
|
||||
4150.020520999988
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 3,
|
||||
"median_ms": 3866.027479999957,
|
||||
"p95_ms": 4150.866962999999,
|
||||
"max_ms": 4150.866962999999,
|
||||
"samples_ms": [
|
||||
2901.4587140000003,
|
||||
3866.027479999957,
|
||||
4150.866962999999
|
||||
]
|
||||
}
|
||||
},
|
||||
"urgent_flush": {
|
||||
"samples": 1,
|
||||
"median_ms": 22.82110700002704,
|
||||
"p95_ms": 22.82110700002704,
|
||||
"max_ms": 22.82110700002704,
|
||||
"samples_ms": [
|
||||
22.82110700002704
|
||||
]
|
||||
},
|
||||
"headline_durably_buffered": true,
|
||||
"activation_drained_queue": true,
|
||||
"personal_archive_records": 26,
|
||||
"muc_archive_records": 100006,
|
||||
"wire_body_deliveries": 27,
|
||||
"status": "pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
{
|
||||
"binary_sha256": "c57757476f2019c60ac877e4a66364b17283218e3cc244ae3f29cde1d3be2977",
|
||||
"scope": "loopback STARTTLS; isolated fixture; no production latency claim",
|
||||
"cases": [
|
||||
{
|
||||
"mam_enabled": false,
|
||||
"sm_enabled": true,
|
||||
"rounds": 5,
|
||||
"seeded_muc_archive_records": 0,
|
||||
"active": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 2.2083669999801714,
|
||||
"p95_ms": 2.7524509999921065,
|
||||
"max_ms": 2.7524509999921065,
|
||||
"samples_ms": [
|
||||
2.0349779999833117,
|
||||
2.1022979999543168,
|
||||
2.2083669999801714,
|
||||
2.581427999984953,
|
||||
2.7524509999921065
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.559320999987904,
|
||||
"p95_ms": 5.448408999995991,
|
||||
"max_ms": 5.448408999995991,
|
||||
"samples_ms": [
|
||||
3.9975349999394894,
|
||||
4.301345000044421,
|
||||
4.559320999987904,
|
||||
4.565895999917302,
|
||||
5.448408999995991
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.29816700000174,
|
||||
"p95_ms": 11.835469999937231,
|
||||
"max_ms": 11.835469999937231,
|
||||
"samples_ms": [
|
||||
3.717826999945828,
|
||||
3.95383000000038,
|
||||
4.29816700000174,
|
||||
11.782627000002321,
|
||||
11.835469999937231
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.366868000033719,
|
||||
"p95_ms": 11.859378999929504,
|
||||
"max_ms": 11.859378999929504,
|
||||
"samples_ms": [
|
||||
3.7456439999914437,
|
||||
4.002205000006143,
|
||||
4.366868000033719,
|
||||
11.841935000006742,
|
||||
11.859378999929504
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 2.1559959999422063,
|
||||
"p95_ms": 2.7428159999089985,
|
||||
"max_ms": 2.7428159999089985,
|
||||
"samples_ms": [
|
||||
1.889266000034695,
|
||||
2.07170299995596,
|
||||
2.1559959999422063,
|
||||
2.2102450000147655,
|
||||
2.7428159999089985
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.291513999987728,
|
||||
"p95_ms": 4.628346999993482,
|
||||
"max_ms": 4.628346999993482,
|
||||
"samples_ms": [
|
||||
4.097702999956709,
|
||||
4.117672000006678,
|
||||
4.291513999987728,
|
||||
4.321024999967449,
|
||||
4.628346999993482
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 3.607771000019966,
|
||||
"p95_ms": 4.329054999971049,
|
||||
"max_ms": 4.329054999971049,
|
||||
"samples_ms": [
|
||||
3.0511659999774565,
|
||||
3.3668879999595447,
|
||||
3.607771000019966,
|
||||
3.629876999980297,
|
||||
4.329054999971049
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 3.669854000008854,
|
||||
"p95_ms": 4.375101999926301,
|
||||
"max_ms": 4.375101999926301,
|
||||
"samples_ms": [
|
||||
3.109387999984392,
|
||||
3.436124000018026,
|
||||
3.669854000008854,
|
||||
3.692369999953371,
|
||||
4.375101999926301
|
||||
]
|
||||
}
|
||||
},
|
||||
"urgent_flush": {
|
||||
"samples": 1,
|
||||
"median_ms": 3.450391999990643,
|
||||
"p95_ms": 3.450391999990643,
|
||||
"max_ms": 3.450391999990643,
|
||||
"samples_ms": [
|
||||
3.450391999990643
|
||||
]
|
||||
},
|
||||
"headline_durably_buffered": true,
|
||||
"activation_drained_queue": true,
|
||||
"personal_archive_records": 0,
|
||||
"muc_archive_records": 0,
|
||||
"wire_body_deliveries": 43,
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"mam_enabled": true,
|
||||
"sm_enabled": true,
|
||||
"rounds": 5,
|
||||
"seeded_muc_archive_records": 0,
|
||||
"active": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 5.680922999999893,
|
||||
"p95_ms": 5.92475899998135,
|
||||
"max_ms": 5.92475899998135,
|
||||
"samples_ms": [
|
||||
4.959245000009105,
|
||||
5.657132000010279,
|
||||
5.680922999999893,
|
||||
5.683216000079483,
|
||||
5.92475899998135
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 10.62067800000932,
|
||||
"p95_ms": 11.266418999980488,
|
||||
"max_ms": 11.266418999980488,
|
||||
"samples_ms": [
|
||||
9.56820900000821,
|
||||
10.354214000017237,
|
||||
10.62067800000932,
|
||||
11.083625999958713,
|
||||
11.266418999980488
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.932548000056158,
|
||||
"p95_ms": 7.615871999973933,
|
||||
"max_ms": 7.615871999973933,
|
||||
"samples_ms": [
|
||||
4.048796000006405,
|
||||
4.207175000033203,
|
||||
4.932548000056158,
|
||||
5.545106999989002,
|
||||
7.615871999973933
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 5.05425800008652,
|
||||
"p95_ms": 7.6429759999427915,
|
||||
"max_ms": 7.6429759999427915,
|
||||
"samples_ms": [
|
||||
4.07517700000426,
|
||||
4.236438000020826,
|
||||
5.05425800008652,
|
||||
5.570927000007941,
|
||||
7.6429759999427915
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 5.44478100005108,
|
||||
"p95_ms": 5.556554000008873,
|
||||
"max_ms": 5.556554000008873,
|
||||
"samples_ms": [
|
||||
4.727472000013222,
|
||||
4.8114250000708125,
|
||||
5.44478100005108,
|
||||
5.505089000052976,
|
||||
5.556554000008873
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 10.183787000073607,
|
||||
"p95_ms": 10.890477999964787,
|
||||
"max_ms": 10.890477999964787,
|
||||
"samples_ms": [
|
||||
9.888724000006732,
|
||||
9.945942000058494,
|
||||
10.183787000073607,
|
||||
10.306656000011571,
|
||||
10.890477999964787
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 5.1263200000448705,
|
||||
"p95_ms": 5.255117000046994,
|
||||
"max_ms": 5.255117000046994,
|
||||
"samples_ms": [
|
||||
4.305390000013176,
|
||||
5.036372000063238,
|
||||
5.1263200000448705,
|
||||
5.168275000073663,
|
||||
5.255117000046994
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 5.184012999961851,
|
||||
"p95_ms": 5.310040000040317,
|
||||
"max_ms": 5.310040000040317,
|
||||
"samples_ms": [
|
||||
4.368295000062972,
|
||||
5.154129000061403,
|
||||
5.184012999961851,
|
||||
5.191530000047351,
|
||||
5.310040000040317
|
||||
]
|
||||
}
|
||||
},
|
||||
"urgent_flush": {
|
||||
"samples": 1,
|
||||
"median_ms": 5.2187170000479455,
|
||||
"p95_ms": 5.2187170000479455,
|
||||
"max_ms": 5.2187170000479455,
|
||||
"samples_ms": [
|
||||
5.2187170000479455
|
||||
]
|
||||
},
|
||||
"headline_durably_buffered": true,
|
||||
"activation_drained_queue": true,
|
||||
"personal_archive_records": 42,
|
||||
"muc_archive_records": 10,
|
||||
"wire_body_deliveries": 43,
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"mam_enabled": true,
|
||||
"sm_enabled": true,
|
||||
"rounds": 5,
|
||||
"seeded_muc_archive_records": 1000,
|
||||
"active": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 5.179283999950712,
|
||||
"p95_ms": 5.702402999986589,
|
||||
"max_ms": 5.702402999986589,
|
||||
"samples_ms": [
|
||||
4.691908000040712,
|
||||
5.172169999923426,
|
||||
5.179283999950712,
|
||||
5.635785999970722,
|
||||
5.702402999986589
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 9.836759999984679,
|
||||
"p95_ms": 10.818348000043443,
|
||||
"max_ms": 10.818348000043443,
|
||||
"samples_ms": [
|
||||
9.177894999993441,
|
||||
9.73506099990118,
|
||||
9.836759999984679,
|
||||
10.593198000037773,
|
||||
10.818348000043443
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.2178580000609145,
|
||||
"p95_ms": 6.532151999977032,
|
||||
"max_ms": 6.532151999977032,
|
||||
"samples_ms": [
|
||||
3.8601360000711793,
|
||||
4.174319999947329,
|
||||
4.2178580000609145,
|
||||
4.571829000042271,
|
||||
6.532151999977032
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.280302000097436,
|
||||
"p95_ms": 6.605132999993657,
|
||||
"max_ms": 6.605132999993657,
|
||||
"samples_ms": [
|
||||
3.8853840000001583,
|
||||
4.233521999935874,
|
||||
4.280302000097436,
|
||||
4.673529000001508,
|
||||
6.605132999993657
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 5.289302999926804,
|
||||
"p95_ms": 5.9182850000070175,
|
||||
"max_ms": 5.9182850000070175,
|
||||
"samples_ms": [
|
||||
4.637428000023647,
|
||||
4.646944000000985,
|
||||
5.289302999926804,
|
||||
5.336470000088411,
|
||||
5.9182850000070175
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 9.881779000011193,
|
||||
"p95_ms": 11.126878000027318,
|
||||
"max_ms": 11.126878000027318,
|
||||
"samples_ms": [
|
||||
9.666927000012038,
|
||||
9.780869999985953,
|
||||
9.881779000011193,
|
||||
9.893610999938574,
|
||||
11.126878000027318
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 5.337903999929949,
|
||||
"p95_ms": 6.605088000014803,
|
||||
"max_ms": 6.605088000014803,
|
||||
"samples_ms": [
|
||||
4.4430670000110695,
|
||||
4.566451999949095,
|
||||
5.337903999929949,
|
||||
5.398619999937182,
|
||||
6.605088000014803
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 5.365761999996721,
|
||||
"p95_ms": 6.657663000055436,
|
||||
"max_ms": 6.657663000055436,
|
||||
"samples_ms": [
|
||||
4.503487999954814,
|
||||
4.589735999957156,
|
||||
5.365761999996721,
|
||||
5.456013999946663,
|
||||
6.657663000055436
|
||||
]
|
||||
}
|
||||
},
|
||||
"urgent_flush": {
|
||||
"samples": 1,
|
||||
"median_ms": 6.4426129999901605,
|
||||
"p95_ms": 6.4426129999901605,
|
||||
"max_ms": 6.4426129999901605,
|
||||
"samples_ms": [
|
||||
6.4426129999901605
|
||||
]
|
||||
},
|
||||
"headline_durably_buffered": true,
|
||||
"activation_drained_queue": true,
|
||||
"personal_archive_records": 42,
|
||||
"muc_archive_records": 1010,
|
||||
"wire_body_deliveries": 43,
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"mam_enabled": true,
|
||||
"sm_enabled": true,
|
||||
"rounds": 5,
|
||||
"seeded_muc_archive_records": 10000,
|
||||
"active": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 5.5639299999938885,
|
||||
"p95_ms": 6.427136000070277,
|
||||
"max_ms": 6.427136000070277,
|
||||
"samples_ms": [
|
||||
4.3693179999308995,
|
||||
5.252241999983198,
|
||||
5.5639299999938885,
|
||||
6.115242999953807,
|
||||
6.427136000070277
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 10.580320000030952,
|
||||
"p95_ms": 11.34455600004003,
|
||||
"max_ms": 11.34455600004003,
|
||||
"samples_ms": [
|
||||
8.862616999977035,
|
||||
10.534624999991138,
|
||||
10.580320000030952,
|
||||
10.863696000001255,
|
||||
11.34455600004003
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.871052999988024,
|
||||
"p95_ms": 6.248740000046382,
|
||||
"max_ms": 6.248740000046382,
|
||||
"samples_ms": [
|
||||
4.298133000020243,
|
||||
4.364993999956823,
|
||||
4.871052999988024,
|
||||
5.0552000000152475,
|
||||
6.248740000046382
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.894828000033158,
|
||||
"p95_ms": 6.306694000045354,
|
||||
"max_ms": 6.306694000045354,
|
||||
"samples_ms": [
|
||||
4.328496999960407,
|
||||
4.393286999970769,
|
||||
4.894828000033158,
|
||||
5.0838870000688985,
|
||||
6.306694000045354
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.837865000013153,
|
||||
"p95_ms": 6.154906000006122,
|
||||
"max_ms": 6.154906000006122,
|
||||
"samples_ms": [
|
||||
4.644825000013952,
|
||||
4.6781300000020565,
|
||||
4.837865000013153,
|
||||
6.011097999930826,
|
||||
6.154906000006122
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 9.839443999908326,
|
||||
"p95_ms": 11.105878999956076,
|
||||
"max_ms": 11.105878999956076,
|
||||
"samples_ms": [
|
||||
9.05296900009489,
|
||||
9.732880000001387,
|
||||
9.839443999908326,
|
||||
10.890076000009685,
|
||||
11.105878999956076
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.868799000064428,
|
||||
"p95_ms": 5.506852999928924,
|
||||
"max_ms": 5.506852999928924,
|
||||
"samples_ms": [
|
||||
4.652097999951366,
|
||||
4.654195000057371,
|
||||
4.868799000064428,
|
||||
5.197563999900012,
|
||||
5.506852999928924
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.921641999999338,
|
||||
"p95_ms": 5.567783999936182,
|
||||
"max_ms": 5.567783999936182,
|
||||
"samples_ms": [
|
||||
4.707422999899791,
|
||||
4.71425300008832,
|
||||
4.921641999999338,
|
||||
5.261014999973668,
|
||||
5.567783999936182
|
||||
]
|
||||
}
|
||||
},
|
||||
"urgent_flush": {
|
||||
"samples": 1,
|
||||
"median_ms": 6.844148000027417,
|
||||
"p95_ms": 6.844148000027417,
|
||||
"max_ms": 6.844148000027417,
|
||||
"samples_ms": [
|
||||
6.844148000027417
|
||||
]
|
||||
},
|
||||
"headline_durably_buffered": true,
|
||||
"activation_drained_queue": true,
|
||||
"personal_archive_records": 42,
|
||||
"muc_archive_records": 10010,
|
||||
"wire_body_deliveries": 43,
|
||||
"status": "pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
{
|
||||
"binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
|
||||
"scope": "loopback STARTTLS; isolated fixture; no production latency claim",
|
||||
"cases": [
|
||||
{
|
||||
"mam_enabled": false,
|
||||
"sm_enabled": true,
|
||||
"rounds": 5,
|
||||
"seeded_muc_archive_records": 0,
|
||||
"active": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 5.10814599999776,
|
||||
"p95_ms": 6.436508999968282,
|
||||
"max_ms": 6.436508999968282,
|
||||
"samples_ms": [
|
||||
4.687508000017715,
|
||||
5.018378999977813,
|
||||
5.10814599999776,
|
||||
5.499873000019306,
|
||||
6.436508999968282
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 10.949089000007461,
|
||||
"p95_ms": 11.194100000011531,
|
||||
"max_ms": 11.194100000011531,
|
||||
"samples_ms": [
|
||||
9.188771000026463,
|
||||
9.251739999967867,
|
||||
10.949089000007461,
|
||||
11.097223999968264,
|
||||
11.194100000011531
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 6.3955159999977695,
|
||||
"p95_ms": 10.028454999996939,
|
||||
"max_ms": 10.028454999996939,
|
||||
"samples_ms": [
|
||||
5.6768890000284955,
|
||||
6.070047000036993,
|
||||
6.3955159999977695,
|
||||
6.783154999993712,
|
||||
10.028454999996939
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 7.227664999959416,
|
||||
"p95_ms": 10.73728499994786,
|
||||
"max_ms": 10.73728499994786,
|
||||
"samples_ms": [
|
||||
6.249474000014743,
|
||||
6.710089000023345,
|
||||
7.227664999959416,
|
||||
7.554007000010188,
|
||||
10.73728499994786
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 4.543079999962174,
|
||||
"p95_ms": 5.169351999995797,
|
||||
"max_ms": 5.169351999995797,
|
||||
"samples_ms": [
|
||||
3.9885659999754353,
|
||||
4.165818999979365,
|
||||
4.543079999962174,
|
||||
4.987367000012455,
|
||||
5.169351999995797
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 9.515092000015102,
|
||||
"p95_ms": 12.47698700001365,
|
||||
"max_ms": 12.47698700001365,
|
||||
"samples_ms": [
|
||||
8.106691999955729,
|
||||
8.858637999992425,
|
||||
9.515092000015102,
|
||||
9.874779000028866,
|
||||
12.47698700001365
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 5.794529000013426,
|
||||
"p95_ms": 7.275404999973034,
|
||||
"max_ms": 7.275404999973034,
|
||||
"samples_ms": [
|
||||
5.51126799996382,
|
||||
5.724920000034217,
|
||||
5.794529000013426,
|
||||
6.444504000000961,
|
||||
7.275404999973034
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 6.479804000036893,
|
||||
"p95_ms": 8.207588000004762,
|
||||
"max_ms": 8.207588000004762,
|
||||
"samples_ms": [
|
||||
6.077395999966484,
|
||||
6.479181999964112,
|
||||
6.479804000036893,
|
||||
7.2692859999961,
|
||||
8.207588000004762
|
||||
]
|
||||
}
|
||||
},
|
||||
"urgent_flush": {
|
||||
"samples": 1,
|
||||
"median_ms": 4.576559000042835,
|
||||
"p95_ms": 4.576559000042835,
|
||||
"max_ms": 4.576559000042835,
|
||||
"samples_ms": [
|
||||
4.576559000042835
|
||||
]
|
||||
},
|
||||
"headline_durably_buffered": true,
|
||||
"activation_drained_queue": true,
|
||||
"personal_archive_records": 0,
|
||||
"muc_archive_records": 0,
|
||||
"wire_body_deliveries": 43,
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"mam_enabled": true,
|
||||
"sm_enabled": true,
|
||||
"rounds": 5,
|
||||
"seeded_muc_archive_records": 0,
|
||||
"active": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 8.454973000027621,
|
||||
"p95_ms": 9.086850000016966,
|
||||
"max_ms": 9.086850000016966,
|
||||
"samples_ms": [
|
||||
7.875873999978467,
|
||||
7.919512999990275,
|
||||
8.454973000027621,
|
||||
8.744820000003983,
|
||||
9.086850000016966
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 15.02084100002321,
|
||||
"p95_ms": 18.49304000000984,
|
||||
"max_ms": 18.49304000000984,
|
||||
"samples_ms": [
|
||||
14.658381000003828,
|
||||
15.0033279999775,
|
||||
15.02084100002321,
|
||||
16.241458000024522,
|
||||
18.49304000000984
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 8.695557000010012,
|
||||
"p95_ms": 10.132943000030536,
|
||||
"max_ms": 10.132943000030536,
|
||||
"samples_ms": [
|
||||
6.247895000001336,
|
||||
8.190369000033115,
|
||||
8.695557000010012,
|
||||
8.886856000003718,
|
||||
10.132943000030536
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 9.425391000036143,
|
||||
"p95_ms": 11.220557999990888,
|
||||
"max_ms": 11.220557999990888,
|
||||
"samples_ms": [
|
||||
6.928168999991158,
|
||||
9.00288799999771,
|
||||
9.425391000036143,
|
||||
9.836486999972749,
|
||||
11.220557999990888
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 8.574552000027325,
|
||||
"p95_ms": 11.412799000027007,
|
||||
"max_ms": 11.412799000027007,
|
||||
"samples_ms": [
|
||||
7.429673999979514,
|
||||
8.434421999993447,
|
||||
8.574552000027325,
|
||||
10.991448000027049,
|
||||
11.412799000027007
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 16.801955000005364,
|
||||
"p95_ms": 20.960896000019602,
|
||||
"max_ms": 20.960896000019602,
|
||||
"samples_ms": [
|
||||
15.289625999969303,
|
||||
16.07350099999394,
|
||||
16.801955000005364,
|
||||
19.067943000038667,
|
||||
20.960896000019602
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 9.673966000036671,
|
||||
"p95_ms": 10.545491000016227,
|
||||
"max_ms": 10.545491000016227,
|
||||
"samples_ms": [
|
||||
8.954777999974795,
|
||||
9.244574000035755,
|
||||
9.673966000036671,
|
||||
10.407988999986628,
|
||||
10.545491000016227
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 10.7329940000227,
|
||||
"p95_ms": 11.54250500002263,
|
||||
"max_ms": 11.54250500002263,
|
||||
"samples_ms": [
|
||||
9.77221699997699,
|
||||
10.012581000012233,
|
||||
10.7329940000227,
|
||||
11.414449999961107,
|
||||
11.54250500002263
|
||||
]
|
||||
}
|
||||
},
|
||||
"urgent_flush": {
|
||||
"samples": 1,
|
||||
"median_ms": 8.784937999962494,
|
||||
"p95_ms": 8.784937999962494,
|
||||
"max_ms": 8.784937999962494,
|
||||
"samples_ms": [
|
||||
8.784937999962494
|
||||
]
|
||||
},
|
||||
"headline_durably_buffered": true,
|
||||
"activation_drained_queue": true,
|
||||
"personal_archive_records": 42,
|
||||
"muc_archive_records": 10,
|
||||
"wire_body_deliveries": 43,
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"mam_enabled": true,
|
||||
"sm_enabled": true,
|
||||
"rounds": 5,
|
||||
"seeded_muc_archive_records": 1000,
|
||||
"active": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 7.590319999962958,
|
||||
"p95_ms": 8.892007999975249,
|
||||
"max_ms": 8.892007999975249,
|
||||
"samples_ms": [
|
||||
5.31427799995754,
|
||||
6.9214119999969625,
|
||||
7.590319999962958,
|
||||
7.624421999992137,
|
||||
8.892007999975249
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 13.376056000026892,
|
||||
"p95_ms": 15.464272999963669,
|
||||
"max_ms": 15.464272999963669,
|
||||
"samples_ms": [
|
||||
12.23329399999784,
|
||||
13.25195099997245,
|
||||
13.376056000026892,
|
||||
14.078856999958589,
|
||||
15.464272999963669
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 31.94280399998206,
|
||||
"p95_ms": 35.83487499997773,
|
||||
"max_ms": 35.83487499997773,
|
||||
"samples_ms": [
|
||||
30.683405999980096,
|
||||
31.05138800003715,
|
||||
31.94280399998206,
|
||||
34.427220999987185,
|
||||
35.83487499997773
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 32.718170999999074,
|
||||
"p95_ms": 36.99477299994669,
|
||||
"max_ms": 36.99477299994669,
|
||||
"samples_ms": [
|
||||
31.32484000002478,
|
||||
31.708390999995117,
|
||||
32.718170999999074,
|
||||
35.15474099998528,
|
||||
36.99477299994669
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 6.955795000010312,
|
||||
"p95_ms": 7.9194580000034875,
|
||||
"max_ms": 7.9194580000034875,
|
||||
"samples_ms": [
|
||||
6.15931200002251,
|
||||
6.666912000014236,
|
||||
6.955795000010312,
|
||||
7.675652999978411,
|
||||
7.9194580000034875
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 13.159411000003729,
|
||||
"p95_ms": 15.27061000001595,
|
||||
"max_ms": 15.27061000001595,
|
||||
"samples_ms": [
|
||||
12.274962999981653,
|
||||
13.057566999975734,
|
||||
13.159411000003729,
|
||||
14.08330199996044,
|
||||
15.27061000001595
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 34.13865300001362,
|
||||
"p95_ms": 36.99340499997561,
|
||||
"max_ms": 36.99340499997561,
|
||||
"samples_ms": [
|
||||
30.982952999977442,
|
||||
32.71812099995941,
|
||||
34.13865300001362,
|
||||
35.38880100001052,
|
||||
36.99340499997561
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 36.06873300003599,
|
||||
"p95_ms": 38.31650299997591,
|
||||
"max_ms": 38.31650299997591,
|
||||
"samples_ms": [
|
||||
33.48175599995784,
|
||||
34.2965520000007,
|
||||
36.06873300003599,
|
||||
36.353186000042115,
|
||||
38.31650299997591
|
||||
]
|
||||
}
|
||||
},
|
||||
"urgent_flush": {
|
||||
"samples": 1,
|
||||
"median_ms": 12.115028999971855,
|
||||
"p95_ms": 12.115028999971855,
|
||||
"max_ms": 12.115028999971855,
|
||||
"samples_ms": [
|
||||
12.115028999971855
|
||||
]
|
||||
},
|
||||
"headline_durably_buffered": true,
|
||||
"activation_drained_queue": true,
|
||||
"personal_archive_records": 42,
|
||||
"muc_archive_records": 1010,
|
||||
"wire_body_deliveries": 43,
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"mam_enabled": true,
|
||||
"sm_enabled": true,
|
||||
"rounds": 5,
|
||||
"seeded_muc_archive_records": 10000,
|
||||
"active": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 9.04404400000658,
|
||||
"p95_ms": 12.927059999981338,
|
||||
"max_ms": 12.927059999981338,
|
||||
"samples_ms": [
|
||||
8.594286000004558,
|
||||
9.04279899998528,
|
||||
9.04404400000658,
|
||||
10.732668000002832,
|
||||
12.927059999981338
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 18.81452300000319,
|
||||
"p95_ms": 21.198468999955367,
|
||||
"max_ms": 21.198468999955367,
|
||||
"samples_ms": [
|
||||
17.06366100000878,
|
||||
17.834419999985585,
|
||||
18.81452300000319,
|
||||
18.89581300002874,
|
||||
21.198468999955367
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 258.5251909999897,
|
||||
"p95_ms": 269.03445800002146,
|
||||
"max_ms": 269.03445800002146,
|
||||
"samples_ms": [
|
||||
243.74590799999396,
|
||||
244.56996899999695,
|
||||
258.5251909999897,
|
||||
260.5109279999738,
|
||||
269.03445800002146
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 259.47216499997694,
|
||||
"p95_ms": 269.0669140000068,
|
||||
"max_ms": 269.0669140000068,
|
||||
"samples_ms": [
|
||||
244.58446500000264,
|
||||
245.61970199999905,
|
||||
259.47216499997694,
|
||||
261.3186459999497,
|
||||
269.0669140000068
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"direct_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 9.321043999989342,
|
||||
"p95_ms": 11.691385000005994,
|
||||
"max_ms": 11.691385000005994,
|
||||
"samples_ms": [
|
||||
9.130955999978596,
|
||||
9.257138999998915,
|
||||
9.321043999989342,
|
||||
9.393109000029654,
|
||||
11.691385000005994
|
||||
]
|
||||
},
|
||||
"direct_application_rtt": {
|
||||
"samples": 5,
|
||||
"median_ms": 22.537706999969487,
|
||||
"p95_ms": 22.7979030000256,
|
||||
"max_ms": 22.7979030000256,
|
||||
"samples_ms": [
|
||||
18.378415999961817,
|
||||
18.92694800000072,
|
||||
22.537706999969487,
|
||||
22.614556000007724,
|
||||
22.7979030000256
|
||||
]
|
||||
},
|
||||
"muc_sender_echo": {
|
||||
"samples": 5,
|
||||
"median_ms": 257.6139750000266,
|
||||
"p95_ms": 278.14462699996056,
|
||||
"max_ms": 278.14462699996056,
|
||||
"samples_ms": [
|
||||
248.2455429999959,
|
||||
251.84251700000004,
|
||||
257.6139750000266,
|
||||
262.41991000000553,
|
||||
278.14462699996056
|
||||
]
|
||||
},
|
||||
"muc_peer": {
|
||||
"samples": 5,
|
||||
"median_ms": 258.46415800003797,
|
||||
"p95_ms": 278.8930929999651,
|
||||
"max_ms": 278.8930929999651,
|
||||
"samples_ms": [
|
||||
250.01428399997394,
|
||||
253.1065140000237,
|
||||
258.46415800003797,
|
||||
263.1856349999566,
|
||||
278.8930929999651
|
||||
]
|
||||
}
|
||||
},
|
||||
"urgent_flush": {
|
||||
"samples": 1,
|
||||
"median_ms": 11.461830000030204,
|
||||
"p95_ms": 11.461830000030204,
|
||||
"max_ms": 11.461830000030204,
|
||||
"samples_ms": [
|
||||
11.461830000030204
|
||||
]
|
||||
},
|
||||
"headline_durably_buffered": true,
|
||||
"activation_drained_queue": true,
|
||||
"personal_archive_records": 42,
|
||||
"muc_archive_records": 10010,
|
||||
"wire_body_deliveries": 43,
|
||||
"status": "pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"clients": 500,
|
||||
"accounts": 100,
|
||||
"rooms": 10,
|
||||
"message_rounds": 3,
|
||||
"concurrency": 20,
|
||||
"binary": "/tmp/xmpp-durable-muc-load-p95vsvii/rust-xmpp",
|
||||
"binary_sha256": "c57757476f2019c60ac877e4a66364b17283218e3cc244ae3f29cde1d3be2977",
|
||||
"before": {
|
||||
"rss_kib": 10076,
|
||||
"cpu_seconds": 0.0,
|
||||
"fd_count": 12
|
||||
},
|
||||
"connect_seconds": 3.149,
|
||||
"connect_latency": {
|
||||
"samples": 500,
|
||||
"median_ms": 121.817,
|
||||
"p95_ms": 169.081,
|
||||
"max_ms": 257.299
|
||||
},
|
||||
"phase": "completed",
|
||||
"status": "pass",
|
||||
"elapsed_seconds": 39.535,
|
||||
"join_seconds": 25.54,
|
||||
"joined": {
|
||||
"rss_kib": 42224,
|
||||
"cpu_seconds": 14.41,
|
||||
"fd_count": 512
|
||||
},
|
||||
"join_presence_deliveries": 25000,
|
||||
"slots_joined": {
|
||||
"count": 500,
|
||||
"bytes": 309790
|
||||
},
|
||||
"message_seconds": 5.504,
|
||||
"room_messages_sent": 30,
|
||||
"room_messages_delivered": 1500,
|
||||
"before_kill": {
|
||||
"rss_kib": 42224,
|
||||
"cpu_seconds": 17.88,
|
||||
"fd_count": 512
|
||||
},
|
||||
"slots_before_kill": {
|
||||
"count": 500,
|
||||
"bytes": 514030
|
||||
},
|
||||
"restart_seconds": 2.962,
|
||||
"resumed_resources": 10,
|
||||
"replayed_pending_room_messages": 10,
|
||||
"membership_groupchat_deliveries_after_resume": 10,
|
||||
"after_resume": {
|
||||
"rss_kib": 20796,
|
||||
"cpu_seconds": 2.81,
|
||||
"fd_count": 22
|
||||
},
|
||||
"slots_after_resume": {
|
||||
"count": 500,
|
||||
"bytes": 671070
|
||||
},
|
||||
"total_seconds": 39.535,
|
||||
"peak_rss_kib": 42240
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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,2 @@
|
||||
Checking rust-xmpp v0.8.3 (/workspace/scratch/362577cdf2e0/recovered/rust-xmpp)
|
||||
Finished `dev` profile [unoptimized] target(s) in 14.04s
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"status": "pass",
|
||||
"delay_ms_per_sync": 100,
|
||||
"acknowledged_messages": 3,
|
||||
"mam_records_per_account": [
|
||||
3,
|
||||
3
|
||||
],
|
||||
"resumed_resources": 4,
|
||||
"replayed_personal_deliveries": 9,
|
||||
"offline_checkpoint": {
|
||||
"exported_sessions": 4,
|
||||
"native_archives": 2,
|
||||
"journals_removed": true,
|
||||
"second_invocation_idempotent": true
|
||||
},
|
||||
"pending_before_reauthentication": [
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
3
|
||||
],
|
||||
"sync_between_publication_and_sigkill": {
|
||||
"total": 32,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 2,
|
||||
"mam:file:fsync": 10,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:file:fdatasync": 16
|
||||
}
|
||||
},
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"scope": "loopback STARTTLS; real delayed fsync/fdatasync; sender SM acknowledgement then SIGKILL; authoritative queues and MAM checked before reauthentication; at-least-once replay"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
Compiling rust-xmpp v0.8.3 (/workspace/scratch/362577cdf2e0/recovered/rust-xmpp)
|
||||
Finished `test` profile [unoptimized] target(s) in 3.26s
|
||||
Running tests/archive_mam.rs (target/debug/deps/archive_mam-1243a5e0f6034fe2)
|
||||
|
||||
running 1 test
|
||||
test archive_larger_than_128mib_streams ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 13 filtered out; finished in 2.76s
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"clients": 500,
|
||||
"accounts": 100,
|
||||
"rooms": 10,
|
||||
"message_rounds": 3,
|
||||
"concurrency": 20,
|
||||
"binary": "/tmp/xmpp-durable-muc-load-ssixqqk9/rust-xmpp",
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"before": {
|
||||
"rss_kib": 9516,
|
||||
"cpu_seconds": 0.01,
|
||||
"fd_count": 12
|
||||
},
|
||||
"connect_seconds": 12.048,
|
||||
"connect_latency": {
|
||||
"samples": 500,
|
||||
"median_ms": 398.044,
|
||||
"p95_ms": 1086.236,
|
||||
"max_ms": 1403.7
|
||||
},
|
||||
"phase": "completed",
|
||||
"status": "pass",
|
||||
"elapsed_seconds": 128.38,
|
||||
"join_seconds": 94.493,
|
||||
"joined": {
|
||||
"rss_kib": 41828,
|
||||
"cpu_seconds": 55.61,
|
||||
"fd_count": 513
|
||||
},
|
||||
"join_presence_deliveries": 25000,
|
||||
"slots_joined": {
|
||||
"count": 500,
|
||||
"bytes": 309790
|
||||
},
|
||||
"message_seconds": 9.585,
|
||||
"room_messages_sent": 30,
|
||||
"room_messages_delivered": 1500,
|
||||
"before_kill": {
|
||||
"rss_kib": 41740,
|
||||
"cpu_seconds": 62.6,
|
||||
"fd_count": 512
|
||||
},
|
||||
"slots_before_kill": {
|
||||
"count": 500,
|
||||
"bytes": 514030
|
||||
},
|
||||
"restart_seconds": 7.557,
|
||||
"resumed_resources": 10,
|
||||
"replayed_pending_room_messages": 10,
|
||||
"membership_groupchat_deliveries_after_resume": 10,
|
||||
"after_resume": {
|
||||
"rss_kib": 20896,
|
||||
"cpu_seconds": 7.09,
|
||||
"fd_count": 22
|
||||
},
|
||||
"slots_after_resume": {
|
||||
"count": 500,
|
||||
"bytes": 671070
|
||||
},
|
||||
"total_seconds": 128.38,
|
||||
"peak_rss_kib": 41852
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"clients": 500,
|
||||
"accounts": 100,
|
||||
"rooms": 10,
|
||||
"message_rounds": 3,
|
||||
"concurrency": 20,
|
||||
"binary": "/tmp/xmpp-durable-muc-load-wgcesopv/rust-xmpp",
|
||||
"binary_sha256": "130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5",
|
||||
"before": {
|
||||
"rss_kib": 9468,
|
||||
"cpu_seconds": 0.0,
|
||||
"fd_count": 12
|
||||
},
|
||||
"connect_seconds": 8.677,
|
||||
"connect_latency": {
|
||||
"samples": 500,
|
||||
"median_ms": 337.216,
|
||||
"p95_ms": 475.311,
|
||||
"max_ms": 632.696
|
||||
},
|
||||
"phase": "completed",
|
||||
"status": "pass",
|
||||
"elapsed_seconds": 151.334,
|
||||
"join_seconds": 104.226,
|
||||
"joined": {
|
||||
"rss_kib": 41468,
|
||||
"cpu_seconds": 39.74,
|
||||
"fd_count": 513
|
||||
},
|
||||
"join_presence_deliveries": 25000,
|
||||
"slots_joined": {
|
||||
"count": 500,
|
||||
"bytes": 309790
|
||||
},
|
||||
"message_seconds": 17.857,
|
||||
"room_messages_sent": 30,
|
||||
"room_messages_delivered": 1500,
|
||||
"before_kill": {
|
||||
"rss_kib": 41464,
|
||||
"cpu_seconds": 48.58,
|
||||
"fd_count": 512
|
||||
},
|
||||
"slots_before_kill": {
|
||||
"count": 500,
|
||||
"bytes": 514030
|
||||
},
|
||||
"restart_seconds": 10.083,
|
||||
"resumed_resources": 10,
|
||||
"replayed_pending_room_messages": 10,
|
||||
"membership_groupchat_deliveries_after_resume": 10,
|
||||
"after_resume": {
|
||||
"rss_kib": 20660,
|
||||
"cpu_seconds": 9.73,
|
||||
"fd_count": 22
|
||||
},
|
||||
"slots_after_resume": {
|
||||
"count": 500,
|
||||
"bytes": 671070
|
||||
},
|
||||
"total_seconds": 151.334,
|
||||
"peak_rss_kib": 41476
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"status": "fail",
|
||||
"cases": [],
|
||||
"binary_sha256": "a5f5b3a552be44fb8ab8b6525dc228bc2dc97f81aecfbc559e12cc29b3e8c00b",
|
||||
"failure": ".rredo recovery skipped journal sync and falsely reported successful handoff"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"status": "fail",
|
||||
"cases": [],
|
||||
"binary_sha256": "a5f5b3a552be44fb8ab8b6525dc228bc2dc97f81aecfbc559e12cc29b3e8c00b",
|
||||
"failure": ".wal recovery skipped journal sync and falsely reported successful handoff"
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"binary_source": "/workspace/scratch/362577cdf2e0/recovered/rust-xmpp/target/release/rust-xmpp",
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"scope": "real loopback TCP/STARTTLS/SASL2 Bind2; isolated personal MAM fixture; no production latency claim",
|
||||
"timing": "raw ordered samples; auth timer starts before authenticate and ends at parsed success; full timer includes TCP/STARTTLS/features; metadata IQ timer runs from send to parsed result",
|
||||
"result": {
|
||||
"status": "pass",
|
||||
"seeded_personal_archive_records": 10000,
|
||||
"archive_bytes": 2138890,
|
||||
"archive_expires_after": "never",
|
||||
"index_present_before_start": false,
|
||||
"reconnect_samples": 2,
|
||||
"expected_metadata": {
|
||||
"start": {
|
||||
"id": "seed-0",
|
||||
"timestamp": "2026-01-01T00:00:00Z"
|
||||
},
|
||||
"end": {
|
||||
"id": "seed-9999",
|
||||
"timestamp": "2026-01-01T02:46:39Z"
|
||||
}
|
||||
},
|
||||
"observed_metadata": [
|
||||
{
|
||||
"start": {
|
||||
"id": "seed-0",
|
||||
"timestamp": "2026-01-01T00:00:00Z"
|
||||
},
|
||||
"end": {
|
||||
"id": "seed-9999",
|
||||
"timestamp": "2026-01-01T02:46:39Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": {
|
||||
"id": "seed-0",
|
||||
"timestamp": "2026-01-01T00:00:00Z"
|
||||
},
|
||||
"end": {
|
||||
"id": "seed-9999",
|
||||
"timestamp": "2026-01-01T02:46:39Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"mam_metadata_iq": {
|
||||
"samples_ms": [
|
||||
1.066574000105902,
|
||||
0.6786549997741531
|
||||
],
|
||||
"median_ms": 0.8726144999400276,
|
||||
"p95_ms": 1.066574000105902,
|
||||
"max_ms": 1.066574000105902
|
||||
},
|
||||
"observed_iq_metadata": [
|
||||
{
|
||||
"start": {
|
||||
"id": "seed-0",
|
||||
"timestamp": "2026-01-01T00:00:00Z"
|
||||
},
|
||||
"end": {
|
||||
"id": "seed-9999",
|
||||
"timestamp": "2026-01-01T02:46:39Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": {
|
||||
"id": "seed-0",
|
||||
"timestamp": "2026-01-01T00:00:00Z"
|
||||
},
|
||||
"end": {
|
||||
"id": "seed-9999",
|
||||
"timestamp": "2026-01-01T02:46:39Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"sasl2_bind2_success": {
|
||||
"samples_ms": [
|
||||
5.694160000075499,
|
||||
4.350195999904827
|
||||
],
|
||||
"median_ms": 5.022177999990163,
|
||||
"p95_ms": 5.694160000075499,
|
||||
"max_ms": 5.694160000075499
|
||||
},
|
||||
"tcp_starttls_to_bind2_success": {
|
||||
"samples_ms": [
|
||||
51.93691700014824,
|
||||
9.144003000074008
|
||||
],
|
||||
"median_ms": 30.540460000111125,
|
||||
"p95_ms": 51.93691700014824,
|
||||
"max_ms": 51.93691700014824
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
{
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"scope": "loopback STARTTLS; fixture only; forced CSI dwell explicitly separated",
|
||||
"cases": [
|
||||
{
|
||||
"active": {
|
||||
"delivery": {
|
||||
"samples": 1,
|
||||
"median_ms": 42.84763399982694,
|
||||
"p95_ms": 42.84763399982694,
|
||||
"max_ms": 42.84763399982694,
|
||||
"samples_ms": [
|
||||
42.84763399982694
|
||||
]
|
||||
},
|
||||
"sender_sm_handled": {
|
||||
"samples": 1,
|
||||
"median_ms": 41.45799199977773,
|
||||
"p95_ms": 41.45799199977773,
|
||||
"max_ms": 41.45799199977773,
|
||||
"samples_ms": [
|
||||
41.45799199977773
|
||||
]
|
||||
},
|
||||
"client_receipt_end_to_end": {
|
||||
"samples": 1,
|
||||
"median_ms": 51.320845000191184,
|
||||
"p95_ms": 51.320845000191184,
|
||||
"max_ms": 51.320845000191184,
|
||||
"samples_ms": [
|
||||
51.320845000191184
|
||||
]
|
||||
},
|
||||
"receipt_return_leg": {
|
||||
"samples": 1,
|
||||
"median_ms": 8.362257000044337,
|
||||
"p95_ms": 8.362257000044337,
|
||||
"max_ms": 8.362257000044337,
|
||||
"samples_ms": [
|
||||
8.362257000044337
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"delivery": {
|
||||
"samples": 1,
|
||||
"median_ms": 7.461921999947663,
|
||||
"p95_ms": 7.461921999947663,
|
||||
"max_ms": 7.461921999947663,
|
||||
"samples_ms": [
|
||||
7.461921999947663
|
||||
]
|
||||
},
|
||||
"sender_sm_handled": {
|
||||
"samples": 1,
|
||||
"median_ms": 6.771670000034646,
|
||||
"p95_ms": 6.771670000034646,
|
||||
"max_ms": 6.771670000034646,
|
||||
"samples_ms": [
|
||||
6.771670000034646
|
||||
]
|
||||
},
|
||||
"client_receipt_end_to_end": {
|
||||
"samples": 1,
|
||||
"median_ms": 12.700967000000674,
|
||||
"p95_ms": 12.700967000000674,
|
||||
"max_ms": 12.700967000000674,
|
||||
"samples_ms": [
|
||||
12.700967000000674
|
||||
]
|
||||
},
|
||||
"receipt_return_leg": {
|
||||
"samples": 1,
|
||||
"median_ms": 5.201973000112048,
|
||||
"p95_ms": 5.201973000112048,
|
||||
"max_ms": 5.201973000112048,
|
||||
"samples_ms": [
|
||||
5.201973000112048
|
||||
]
|
||||
}
|
||||
},
|
||||
"receipt_only-active": {
|
||||
"delivery_ms": 23.464398000214715,
|
||||
"sender_sm_handled_ms": 23.388774000068224,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"receipt_only-inactive": {
|
||||
"delivery_ms": 11.619346000316,
|
||||
"sender_sm_handled_ms": 11.170642000251974,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"omemo_legacy_only-active": {
|
||||
"delivery_ms": 3.3123490002253675,
|
||||
"sender_sm_handled_ms": 3.2682549999663024,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"omemo_legacy_only-inactive": {
|
||||
"delivery_ms": 5.355494000014005,
|
||||
"sender_sm_handled_ms": 4.585807000239583,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"omemo_2_only-active": {
|
||||
"delivery_ms": 6.6929370000252675,
|
||||
"sender_sm_handled_ms": 5.942287999914697,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"omemo_2_only-inactive": {
|
||||
"delivery_ms": 7.484531000045536,
|
||||
"sender_sm_handled_ms": 7.103434999862657,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"omemo_with_eme-active": {
|
||||
"delivery_ms": 8.187372000065807,
|
||||
"sender_sm_handled_ms": 7.642809000117268,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"omemo_with_eme-inactive": {
|
||||
"delivery_ms": 8.972958999947878,
|
||||
"sender_sm_handled_ms": 8.508876000178134,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"marker_received-active": {
|
||||
"delivery_ms": 7.674009000311344,
|
||||
"sender_sm_handled_ms": 7.3800410000330885,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"marker_received-inactive": {
|
||||
"delivery_ms": 5.613406000065879,
|
||||
"sender_sm_handled_ms": 5.646823999995831,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"marker_displayed-active": {
|
||||
"delivery_ms": 5.180439000014303,
|
||||
"sender_sm_handled_ms": 4.853748000186897,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"marker_displayed-inactive": {
|
||||
"delivery_ms": 8.705914999609377,
|
||||
"sender_sm_handled_ms": 8.28981999984535,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"marker_acknowledged-active": {
|
||||
"delivery_ms": 6.910242000230937,
|
||||
"sender_sm_handled_ms": 6.799871000112034,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"marker_acknowledged-inactive": {
|
||||
"delivery_ms": 8.707511000011436,
|
||||
"sender_sm_handled_ms": 8.398837999720854,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"low_priority_headline-active": {
|
||||
"delivery_ms": 4.267847999926744,
|
||||
"sender_sm_handled_ms": 4.236566000145103,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"low_priority_headline-inactive": {
|
||||
"delivery_ms": 108.03854200003116,
|
||||
"sender_sm_handled_ms": 2.853955999853497,
|
||||
"durably_csi_buffered": true,
|
||||
"forced_dwell_seconds": 0.1,
|
||||
"after_activation_ms": 3.106009000021004
|
||||
},
|
||||
"sm_backlog": {
|
||||
"seeded_messages": 30,
|
||||
"durable_pending_before_sample": 30,
|
||||
"delivery_ms": 8.070619000136503,
|
||||
"sender_sm_handled_ms": 7.959626000229036
|
||||
},
|
||||
"status": "pass",
|
||||
"final_personal_mam_records_per_user": [
|
||||
1045,
|
||||
1045
|
||||
],
|
||||
"csi_policy": "default",
|
||||
"baseline_expect_held": false,
|
||||
"roster_contacts_per_user": 0,
|
||||
"seeded_personal_mam_records_per_user": 1000,
|
||||
"archive_policy": "always"
|
||||
},
|
||||
{
|
||||
"active": {
|
||||
"delivery": {
|
||||
"samples": 1,
|
||||
"median_ms": 9.351153999887174,
|
||||
"p95_ms": 9.351153999887174,
|
||||
"max_ms": 9.351153999887174,
|
||||
"samples_ms": [
|
||||
9.351153999887174
|
||||
]
|
||||
},
|
||||
"sender_sm_handled": {
|
||||
"samples": 1,
|
||||
"median_ms": 9.169040999950084,
|
||||
"p95_ms": 9.169040999950084,
|
||||
"max_ms": 9.169040999950084,
|
||||
"samples_ms": [
|
||||
9.169040999950084
|
||||
]
|
||||
},
|
||||
"client_receipt_end_to_end": {
|
||||
"samples": 1,
|
||||
"median_ms": 13.487032000284671,
|
||||
"p95_ms": 13.487032000284671,
|
||||
"max_ms": 13.487032000284671,
|
||||
"samples_ms": [
|
||||
13.487032000284671
|
||||
]
|
||||
},
|
||||
"receipt_return_leg": {
|
||||
"samples": 1,
|
||||
"median_ms": 4.101098000319325,
|
||||
"p95_ms": 4.101098000319325,
|
||||
"max_ms": 4.101098000319325,
|
||||
"samples_ms": [
|
||||
4.101098000319325
|
||||
]
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"delivery": {
|
||||
"samples": 1,
|
||||
"median_ms": 5.198949999794422,
|
||||
"p95_ms": 5.198949999794422,
|
||||
"max_ms": 5.198949999794422,
|
||||
"samples_ms": [
|
||||
5.198949999794422
|
||||
]
|
||||
},
|
||||
"sender_sm_handled": {
|
||||
"samples": 1,
|
||||
"median_ms": 5.164645999684581,
|
||||
"p95_ms": 5.164645999684581,
|
||||
"max_ms": 5.164645999684581,
|
||||
"samples_ms": [
|
||||
5.164645999684581
|
||||
]
|
||||
},
|
||||
"client_receipt_end_to_end": {
|
||||
"samples": 1,
|
||||
"median_ms": 13.508577999800764,
|
||||
"p95_ms": 13.508577999800764,
|
||||
"max_ms": 13.508577999800764,
|
||||
"samples_ms": [
|
||||
13.508577999800764
|
||||
]
|
||||
},
|
||||
"receipt_return_leg": {
|
||||
"samples": 1,
|
||||
"median_ms": 8.226426999954128,
|
||||
"p95_ms": 8.226426999954128,
|
||||
"max_ms": 8.226426999954128,
|
||||
"samples_ms": [
|
||||
8.226426999954128
|
||||
]
|
||||
}
|
||||
},
|
||||
"receipt_only-active": {
|
||||
"delivery_ms": 8.37234799973885,
|
||||
"sender_sm_handled_ms": 7.964516999891202,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"receipt_only-inactive": {
|
||||
"delivery_ms": 109.20343799989496,
|
||||
"sender_sm_handled_ms": 5.452117000004364,
|
||||
"durably_csi_buffered": true,
|
||||
"forced_dwell_seconds": 0.1,
|
||||
"after_activation_ms": 2.879265000046871
|
||||
},
|
||||
"omemo_legacy_only-active": {
|
||||
"delivery_ms": 4.961447999903612,
|
||||
"sender_sm_handled_ms": 4.207620999750361,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"omemo_legacy_only-inactive": {
|
||||
"delivery_ms": 107.77542400001039,
|
||||
"sender_sm_handled_ms": 4.299592999814195,
|
||||
"durably_csi_buffered": true,
|
||||
"forced_dwell_seconds": 0.1,
|
||||
"after_activation_ms": 1.5704200000072888
|
||||
},
|
||||
"omemo_2_only-active": {
|
||||
"delivery_ms": 3.2654749998073385,
|
||||
"sender_sm_handled_ms": 3.3216599999832397,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"omemo_2_only-inactive": {
|
||||
"delivery_ms": 109.47085399993739,
|
||||
"sender_sm_handled_ms": 5.282463000185089,
|
||||
"durably_csi_buffered": true,
|
||||
"forced_dwell_seconds": 0.1,
|
||||
"after_activation_ms": 2.073672999813425
|
||||
},
|
||||
"omemo_with_eme-active": {
|
||||
"delivery_ms": 6.262568000238389,
|
||||
"sender_sm_handled_ms": 6.371549000050436,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"omemo_with_eme-inactive": {
|
||||
"delivery_ms": 5.960880999737128,
|
||||
"sender_sm_handled_ms": 5.667837000146392,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"marker_received-active": {
|
||||
"delivery_ms": 7.435425000039686,
|
||||
"sender_sm_handled_ms": 7.0634520002386125,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"marker_received-inactive": {
|
||||
"delivery_ms": 110.35115000004225,
|
||||
"sender_sm_handled_ms": 5.86408199978905,
|
||||
"durably_csi_buffered": true,
|
||||
"forced_dwell_seconds": 0.1,
|
||||
"after_activation_ms": 2.434945000004518
|
||||
},
|
||||
"marker_displayed-active": {
|
||||
"delivery_ms": 3.6287610000727,
|
||||
"sender_sm_handled_ms": 3.696032999869203,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"marker_displayed-inactive": {
|
||||
"delivery_ms": 112.26732199975231,
|
||||
"sender_sm_handled_ms": 8.923948999836284,
|
||||
"durably_csi_buffered": true,
|
||||
"forced_dwell_seconds": 0.1,
|
||||
"after_activation_ms": 1.43316000003324
|
||||
},
|
||||
"marker_acknowledged-active": {
|
||||
"delivery_ms": 4.8418340002172044,
|
||||
"sender_sm_handled_ms": 4.916578000120353,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"marker_acknowledged-inactive": {
|
||||
"delivery_ms": 110.98447399990619,
|
||||
"sender_sm_handled_ms": 5.764318000274216,
|
||||
"durably_csi_buffered": true,
|
||||
"forced_dwell_seconds": 0.1,
|
||||
"after_activation_ms": 3.3109219998550543
|
||||
},
|
||||
"low_priority_headline-active": {
|
||||
"delivery_ms": 4.907228000320174,
|
||||
"sender_sm_handled_ms": 4.593587000272237,
|
||||
"durably_csi_buffered": false,
|
||||
"forced_dwell_seconds": 0,
|
||||
"after_activation_ms": null
|
||||
},
|
||||
"low_priority_headline-inactive": {
|
||||
"delivery_ms": 106.62631800005329,
|
||||
"sender_sm_handled_ms": 2.990170999964903,
|
||||
"durably_csi_buffered": true,
|
||||
"forced_dwell_seconds": 0.1,
|
||||
"after_activation_ms": 1.6857880000316072
|
||||
},
|
||||
"sm_backlog": {
|
||||
"seeded_messages": 30,
|
||||
"durable_pending_before_sample": 30,
|
||||
"delivery_ms": 8.221272999890061,
|
||||
"sender_sm_handled_ms": 8.186153000224294
|
||||
},
|
||||
"status": "pass",
|
||||
"final_personal_mam_records_per_user": [
|
||||
1045,
|
||||
1045
|
||||
],
|
||||
"csi_policy": "native",
|
||||
"baseline_expect_held": false,
|
||||
"roster_contacts_per_user": 0,
|
||||
"seeded_personal_mam_records_per_user": 1000,
|
||||
"archive_policy": "always"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"status": "pass",
|
||||
"recipient_state": "hibernating",
|
||||
"durable_pending_at_gateway_stream": {
|
||||
"push-first": true,
|
||||
"push-second": true
|
||||
},
|
||||
"observation_seconds": 2,
|
||||
"elapsed_seconds": 2.013597706999917,
|
||||
"gateway_post_tls_stream_observed": true,
|
||||
"gateway_post_tls_stream_at_bound": true,
|
||||
"blocked_stage": "post-tls-features",
|
||||
"durable_pending": {
|
||||
"push-first": true,
|
||||
"push-second": true
|
||||
},
|
||||
"sender_sm_ack_h": [
|
||||
4
|
||||
],
|
||||
"sender_inputs": 4,
|
||||
"sender_handled_before": 2,
|
||||
"binary_version": "rust-xmpp 0.8.3 \u2014 experimental XMPP runtime",
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"scope": "loopback TLS fixture; stalled external XMPP push service; no APNs/FCM"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"status": "pass",
|
||||
"recipient_state": "offline",
|
||||
"durable_pending_at_gateway_stream": {
|
||||
"push-first": true,
|
||||
"push-second": true
|
||||
},
|
||||
"observation_seconds": 2,
|
||||
"elapsed_seconds": 2.1132061280000016,
|
||||
"gateway_post_tls_stream_observed": true,
|
||||
"gateway_post_tls_stream_at_bound": true,
|
||||
"blocked_stage": "post-tls-features",
|
||||
"durable_pending": {
|
||||
"push-first": true,
|
||||
"push-second": true
|
||||
},
|
||||
"sender_sm_ack_h": [
|
||||
4
|
||||
],
|
||||
"sender_inputs": 4,
|
||||
"sender_handled_before": 2,
|
||||
"binary_version": "rust-xmpp 0.8.3 \u2014 experimental XMPP runtime",
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"scope": "loopback TLS fixture; stalled external XMPP push service; no APNs/FCM"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"status": "pass",
|
||||
"cases": [
|
||||
{
|
||||
"suffix": ".wal",
|
||||
"status": "pass",
|
||||
"checkpoint_rejected_sync_failure": true,
|
||||
"journals_preserved_on_failure": 4,
|
||||
"serve_rejected_before_listeners": true,
|
||||
"clean_retry_preserved_sessions": 4,
|
||||
"clean_retry_preserved_archives": 2
|
||||
},
|
||||
{
|
||||
"suffix": ".rredo",
|
||||
"status": "pass",
|
||||
"checkpoint_rejected_sync_failure": true,
|
||||
"journals_preserved_on_failure": 2,
|
||||
"serve_rejected_before_listeners": false,
|
||||
"clean_retry_preserved_sessions": 4,
|
||||
"clean_retry_preserved_archives": 2
|
||||
}
|
||||
],
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
Compiling rust-xmpp v0.8.3 (/workspace/scratch/362577cdf2e0/recovered/rust-xmpp)
|
||||
Finished `release` profile [optimized] target(s) in 1m 43s
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"scope": "loopback STARTTLS; four SM resources/two accounts; MAM + pending avatar metadata PEP; fixture-only fsync/fdatasync including data directories; real sync retained; operation windows include background SM heartbeat",
|
||||
"cases": [
|
||||
{
|
||||
"samples": [
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 2042.56345,
|
||||
"sent": 2239.283932,
|
||||
"received": 2442.564925
|
||||
},
|
||||
"sender_sm_handled_ms": 2441.985416,
|
||||
"publication_plus_delivery_ms": 2442.641984,
|
||||
"sync": {
|
||||
"total": 12,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 2,
|
||||
"mam:file:fsync": 6,
|
||||
"sm:file:fdatasync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 2676.283903,
|
||||
"sent": 2476.200907,
|
||||
"received": 3095.924978
|
||||
},
|
||||
"sender_sm_handled_ms": 3094.949323,
|
||||
"publication_plus_delivery_ms": 3096.115223,
|
||||
"sync": {
|
||||
"total": 15,
|
||||
"by_store_kind_op": {
|
||||
"mam:file:fsync": 2,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:file:fdatasync": 9
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"mam_records_per_account": [
|
||||
102,
|
||||
102
|
||||
],
|
||||
"status": "pass",
|
||||
"delay_ms_per_sync": 200,
|
||||
"server_message_processing_slow_events": [
|
||||
{
|
||||
"duration_ms": 2440,
|
||||
"route": "local",
|
||||
"pre_routing_ms": 0,
|
||||
"sender_mam_ms": 813,
|
||||
"recipient_mam_ms": 816,
|
||||
"delivery_ms": 202,
|
||||
"carbons_ms": 404,
|
||||
"offline_push_ms": 0,
|
||||
"handled_ms": 202,
|
||||
"target_count": 1,
|
||||
"carbon_count": 2
|
||||
},
|
||||
{
|
||||
"duration_ms": 1259,
|
||||
"route": "local",
|
||||
"pre_routing_ms": 0,
|
||||
"sender_mam_ms": 204,
|
||||
"recipient_mam_ms": 208,
|
||||
"delivery_ms": 223,
|
||||
"carbons_ms": 410,
|
||||
"offline_push_ms": 0,
|
||||
"handled_ms": 211,
|
||||
"target_count": 1,
|
||||
"carbon_count": 2
|
||||
}
|
||||
],
|
||||
"server_message_processing_slow_ms": [
|
||||
2440,
|
||||
1259
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"binary_sha256": "130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5",
|
||||
"scope": "loopback STARTTLS; four SM resources/two accounts; MAM + pending avatar PEP; fixture-only fsync/fdatasync including data directories; real sync retained",
|
||||
"cases": [
|
||||
{
|
||||
"samples": [
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 2836.63993,
|
||||
"sent": 3238.840305,
|
||||
"received": 3643.707041
|
||||
},
|
||||
"sender_sm_handled_ms": 3643.297905,
|
||||
"publication_plus_delivery_ms": 3643.772507,
|
||||
"sync": {
|
||||
"total": 24,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"sm:directory:fsync": 8,
|
||||
"sm:file:fsync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 5782.018326,
|
||||
"sent": 5384.85421,
|
||||
"received": 6596.874338
|
||||
},
|
||||
"sender_sm_handled_ms": 6595.924675,
|
||||
"publication_plus_delivery_ms": 6597.492339,
|
||||
"sync": {
|
||||
"total": 30,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:directory:fsync": 9,
|
||||
"sm:file:fsync": 9
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"mam_records_per_account": [
|
||||
102,
|
||||
102
|
||||
],
|
||||
"status": "pass",
|
||||
"delay_ms_per_sync": 200,
|
||||
"server_message_processing_slow_ms": [
|
||||
3245,
|
||||
3235
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"scope": "loopback STARTTLS; four SM resources/two accounts; MAM + pending avatar metadata PEP; fixture-only fsync/fdatasync including data directories; real sync retained; operation windows include background SM heartbeat",
|
||||
"cases": [
|
||||
{
|
||||
"samples": [
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 1083.729337,
|
||||
"sent": 1183.535171,
|
||||
"received": 1301.736052
|
||||
},
|
||||
"sender_sm_handled_ms": 1300.213796,
|
||||
"publication_plus_delivery_ms": 1309.067665,
|
||||
"sync": {
|
||||
"total": 12,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 2,
|
||||
"mam:file:fsync": 6,
|
||||
"sm:file:fdatasync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 1,
|
||||
"delivery_ms": {
|
||||
"direct": 416.413847,
|
||||
"sent": 521.445561,
|
||||
"received": 622.692061
|
||||
},
|
||||
"sender_sm_handled_ms": 622.557259,
|
||||
"publication_plus_delivery_ms": 627.341892,
|
||||
"sync": {
|
||||
"total": 6,
|
||||
"by_store_kind_op": {
|
||||
"mam:file:fsync": 2,
|
||||
"sm:file:fdatasync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 2,
|
||||
"delivery_ms": {
|
||||
"direct": 419.683064,
|
||||
"sent": 551.764241,
|
||||
"received": 644.751777
|
||||
},
|
||||
"sender_sm_handled_ms": 644.082891,
|
||||
"publication_plus_delivery_ms": 644.882621,
|
||||
"sync": {
|
||||
"total": 6,
|
||||
"by_store_kind_op": {
|
||||
"mam:file:fsync": 2,
|
||||
"sm:file:fdatasync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 1376.989787,
|
||||
"sent": 1273.220821,
|
||||
"received": 1478.990457
|
||||
},
|
||||
"sender_sm_handled_ms": 1477.870242,
|
||||
"publication_plus_delivery_ms": 1479.132408,
|
||||
"sync": {
|
||||
"total": 14,
|
||||
"by_store_kind_op": {
|
||||
"mam:file:fsync": 2,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:file:fdatasync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 1,
|
||||
"delivery_ms": {
|
||||
"direct": 1523.59144,
|
||||
"sent": 1288.285577,
|
||||
"received": 1626.311997
|
||||
},
|
||||
"sender_sm_handled_ms": 1626.005979,
|
||||
"publication_plus_delivery_ms": 1626.574287,
|
||||
"sync": {
|
||||
"total": 14,
|
||||
"by_store_kind_op": {
|
||||
"mam:file:fsync": 2,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:file:fdatasync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 2,
|
||||
"delivery_ms": {
|
||||
"direct": 1378.231158,
|
||||
"sent": 1276.210346,
|
||||
"received": 1582.305858
|
||||
},
|
||||
"sender_sm_handled_ms": 1581.975429,
|
||||
"publication_plus_delivery_ms": 1582.463448,
|
||||
"sync": {
|
||||
"total": 15,
|
||||
"by_store_kind_op": {
|
||||
"mam:file:fsync": 2,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:file:fdatasync": 9
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"mam_records_per_account": [
|
||||
106,
|
||||
106
|
||||
],
|
||||
"status": "pass",
|
||||
"delay_ms_per_sync": 100,
|
||||
"server_message_processing_slow_events": [
|
||||
{
|
||||
"duration_ms": 1298,
|
||||
"route": "local",
|
||||
"pre_routing_ms": 0,
|
||||
"sender_mam_ms": 421,
|
||||
"recipient_mam_ms": 416,
|
||||
"delivery_ms": 109,
|
||||
"carbons_ms": 232,
|
||||
"offline_push_ms": 0,
|
||||
"handled_ms": 117,
|
||||
"target_count": 1,
|
||||
"carbon_count": 2
|
||||
},
|
||||
{
|
||||
"duration_ms": 621,
|
||||
"route": "local",
|
||||
"pre_routing_ms": 0,
|
||||
"sender_mam_ms": 101,
|
||||
"recipient_mam_ms": 104,
|
||||
"delivery_ms": 104,
|
||||
"carbons_ms": 206,
|
||||
"offline_push_ms": 0,
|
||||
"handled_ms": 103,
|
||||
"target_count": 1,
|
||||
"carbon_count": 2
|
||||
},
|
||||
{
|
||||
"duration_ms": 638,
|
||||
"route": "local",
|
||||
"pre_routing_ms": 0,
|
||||
"sender_mam_ms": 101,
|
||||
"recipient_mam_ms": 102,
|
||||
"delivery_ms": 104,
|
||||
"carbons_ms": 219,
|
||||
"offline_push_ms": 1,
|
||||
"handled_ms": 108,
|
||||
"target_count": 1,
|
||||
"carbon_count": 2
|
||||
},
|
||||
{
|
||||
"duration_ms": 625,
|
||||
"route": "local",
|
||||
"pre_routing_ms": 1,
|
||||
"sender_mam_ms": 104,
|
||||
"recipient_mam_ms": 102,
|
||||
"delivery_ms": 106,
|
||||
"carbons_ms": 208,
|
||||
"offline_push_ms": 0,
|
||||
"handled_ms": 101,
|
||||
"target_count": 1,
|
||||
"carbon_count": 2
|
||||
},
|
||||
{
|
||||
"duration_ms": 756,
|
||||
"route": "local",
|
||||
"pre_routing_ms": 1,
|
||||
"sender_mam_ms": 105,
|
||||
"recipient_mam_ms": 103,
|
||||
"delivery_ms": 102,
|
||||
"carbons_ms": 340,
|
||||
"offline_push_ms": 0,
|
||||
"handled_ms": 102,
|
||||
"target_count": 1,
|
||||
"carbon_count": 2
|
||||
},
|
||||
{
|
||||
"duration_ms": 617,
|
||||
"route": "local",
|
||||
"pre_routing_ms": 1,
|
||||
"sender_mam_ms": 102,
|
||||
"recipient_mam_ms": 104,
|
||||
"delivery_ms": 101,
|
||||
"carbons_ms": 204,
|
||||
"offline_push_ms": 0,
|
||||
"handled_ms": 101,
|
||||
"target_count": 1,
|
||||
"carbon_count": 2
|
||||
}
|
||||
],
|
||||
"server_message_processing_slow_ms": [
|
||||
1298,
|
||||
621,
|
||||
638,
|
||||
625,
|
||||
756,
|
||||
617
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
{
|
||||
"binary_sha256": "130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5",
|
||||
"scope": "loopback STARTTLS; four SM resources/two accounts; MAM + pending avatar PEP; fixture-only fsync/fdatasync including data directories; real sync retained",
|
||||
"cases": [
|
||||
{
|
||||
"samples": [
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 13.745165,
|
||||
"sent": 16.132819,
|
||||
"received": 18.746494
|
||||
},
|
||||
"sender_sm_handled_ms": 18.699594,
|
||||
"publication_plus_delivery_ms": 18.823329,
|
||||
"sync": {
|
||||
"total": 16,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"sm:directory:fsync": 4,
|
||||
"sm:file:fsync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 1,
|
||||
"delivery_ms": {
|
||||
"direct": 15.636949,
|
||||
"sent": 18.138746,
|
||||
"received": 20.807559
|
||||
},
|
||||
"sender_sm_handled_ms": 20.705943,
|
||||
"publication_plus_delivery_ms": 20.843968,
|
||||
"sync": {
|
||||
"total": 16,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"sm:directory:fsync": 4,
|
||||
"sm:file:fsync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 2,
|
||||
"delivery_ms": {
|
||||
"direct": 21.358818,
|
||||
"sent": 24.788203,
|
||||
"received": 26.125405
|
||||
},
|
||||
"sender_sm_handled_ms": 26.090729,
|
||||
"publication_plus_delivery_ms": 26.187115,
|
||||
"sync": {
|
||||
"total": 16,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"sm:directory:fsync": 4,
|
||||
"sm:file:fsync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 32.67965,
|
||||
"sent": 29.361139,
|
||||
"received": 35.870217
|
||||
},
|
||||
"sender_sm_handled_ms": 35.819252,
|
||||
"publication_plus_delivery_ms": 35.991777,
|
||||
"sync": {
|
||||
"total": 28,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:directory:fsync": 8,
|
||||
"sm:file:fsync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 1,
|
||||
"delivery_ms": {
|
||||
"direct": 30.47626,
|
||||
"sent": 28.128456,
|
||||
"received": 32.654735
|
||||
},
|
||||
"sender_sm_handled_ms": 32.540092,
|
||||
"publication_plus_delivery_ms": 32.738534,
|
||||
"sync": {
|
||||
"total": 28,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:directory:fsync": 8,
|
||||
"sm:file:fsync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 2,
|
||||
"delivery_ms": {
|
||||
"direct": 30.492529,
|
||||
"sent": 28.027238,
|
||||
"received": 33.697165
|
||||
},
|
||||
"sender_sm_handled_ms": 33.576139,
|
||||
"publication_plus_delivery_ms": 33.786933,
|
||||
"sync": {
|
||||
"total": 28,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:directory:fsync": 8,
|
||||
"sm:file:fsync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"mam_records_per_account": [
|
||||
106,
|
||||
106
|
||||
],
|
||||
"status": "pass",
|
||||
"delay_ms_per_sync": 0
|
||||
},
|
||||
{
|
||||
"samples": [
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 1226.407308,
|
||||
"sent": 1431.404759,
|
||||
"received": 1837.301696
|
||||
},
|
||||
"sender_sm_handled_ms": 1633.73631,
|
||||
"publication_plus_delivery_ms": 1837.391548,
|
||||
"sync": {
|
||||
"total": 18,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"sm:directory:fsync": 5,
|
||||
"sm:file:fsync": 5
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 1,
|
||||
"delivery_ms": {
|
||||
"direct": 1234.414585,
|
||||
"sent": 1436.607311,
|
||||
"received": 1642.84668
|
||||
},
|
||||
"sender_sm_handled_ms": 1641.598264,
|
||||
"publication_plus_delivery_ms": 1642.913856,
|
||||
"sync": {
|
||||
"total": 16,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"sm:directory:fsync": 4,
|
||||
"sm:file:fsync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 2,
|
||||
"delivery_ms": {
|
||||
"direct": 1232.468024,
|
||||
"sent": 1441.824206,
|
||||
"received": 1872.009731
|
||||
},
|
||||
"sender_sm_handled_ms": 1652.92941,
|
||||
"publication_plus_delivery_ms": 1872.108448,
|
||||
"sync": {
|
||||
"total": 18,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"sm:directory:fsync": 5,
|
||||
"sm:file:fsync": 5
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 3209.381557,
|
||||
"sent": 3419.876568,
|
||||
"received": 3614.69826
|
||||
},
|
||||
"sender_sm_handled_ms": 3613.565949,
|
||||
"publication_plus_delivery_ms": 3615.484237,
|
||||
"sync": {
|
||||
"total": 28,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:directory:fsync": 8,
|
||||
"sm:file:fsync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 1,
|
||||
"delivery_ms": {
|
||||
"direct": 2665.684412,
|
||||
"sent": 2461.378505,
|
||||
"received": 2870.784696
|
||||
},
|
||||
"sender_sm_handled_ms": 2870.455915,
|
||||
"publication_plus_delivery_ms": 2871.000834,
|
||||
"sync": {
|
||||
"total": 28,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:directory:fsync": 8,
|
||||
"sm:file:fsync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 2,
|
||||
"delivery_ms": {
|
||||
"direct": 2683.292251,
|
||||
"sent": 2475.225974,
|
||||
"received": 3636.635597
|
||||
},
|
||||
"sender_sm_handled_ms": 3195.209226,
|
||||
"publication_plus_delivery_ms": 3640.372412,
|
||||
"sync": {
|
||||
"total": 30,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:directory:fsync": 9,
|
||||
"sm:file:fsync": 9
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"mam_records_per_account": [
|
||||
106,
|
||||
106
|
||||
],
|
||||
"status": "pass",
|
||||
"delay_ms_per_sync": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
Compiling rust-xmpp v0.8.3 (/workspace/scratch/362577cdf2e0/recovered/rust-xmpp)
|
||||
Finished `test` profile [unoptimized] target(s) in 29.67s
|
||||
Running unittests src/lib.rs (target/debug/deps/rust_xmpp-792bf902c86621ec)
|
||||
|
||||
running 50 tests
|
||||
test csi::checkpoint_tests::failed_queue_checkpoints_restore_append_and_drain_without_copying_queue ... ok
|
||||
test federation::tests::external_must_be_in_the_advertised_mechanisms ... ok
|
||||
test federation::tests::starttls_must_be_an_immediate_stream_feature ... ok
|
||||
test csi::checkpoint_tests::only_queue_changes_require_a_checkpoint ... ok
|
||||
test federation::tests::federation_domains_are_dns_names_and_case_insensitive ... ok
|
||||
test federation::tests::stream_restarts_compare_canonical_domains ... ok
|
||||
test http_config::tests::literal_pchars_and_percent_encoding_are_supported ... ok
|
||||
test http_config::tests::invalid_paths_types_urls_and_conflicts_are_errors ... ok
|
||||
test http_config::tests::external_prefix_and_standard_port ... ok
|
||||
test http_config::tests::routes_hosts_and_default_without_overriding_known_hosts ... ok
|
||||
test http_proxy::tests::config_validation_and_network_boundaries ... ok
|
||||
test http_proxy::tests::empty_native_lua_table_disables_proxy_trust ... ok
|
||||
test http_config::tests::mounts_inherit_per_app_and_expand_virtual_host ... ok
|
||||
test http_proxy::tests::forwarded_tls_termination_cannot_upgrade_a_plaintext_chain_hop ... ok
|
||||
test http_proxy::tests::malformed_forwarded_and_unknown_identifiers_are_ignored ... ok
|
||||
test http_proxy::tests::untrusted_socket_ignores_spoofed_headers ... ok
|
||||
test http_proxy::tests::legacy_chain_uses_rightmost_untrusted_and_tls_is_preserved ... ok
|
||||
test http_proxy::tests::forwarded_mode_stops_at_first_untrusted_hop ... ok
|
||||
test http_transport::tests::bosh_processing_capacity_is_bounded ... ok
|
||||
test http_transport::tests::configured_mount_host_origin_and_secure_dispatch ... ok
|
||||
test http_transport::tests::independent_framing ... ok
|
||||
test http_transport::tests::websocket_preserves_inherited_namespaces_and_spaced_declarations ... ok
|
||||
test logging::tests::console_aliases_share_one_sink_with_combined_levels ... ok
|
||||
test sasl::tests::reject_malformed_and_downgrade ... ok
|
||||
test archive::redo_rewrite_tests::crash_gap_after_redo_retirement_before_native_rename_keeps_old_snapshot ... ok
|
||||
test archive::redo_rewrite_tests::crash_gap_after_native_rename_has_no_old_redo_to_resurrect_deletions ... ok
|
||||
test sasl::tests::rfc5802_sha1_vector ... ok
|
||||
test server::authorization_tests::http_interfaces_follow_secure_override_wildcards_and_global_fallback ... ok
|
||||
test server::authorization_tests::pep_diagnostics_count_only_first_item_devices_without_logging_ids ... ok
|
||||
test server::authorization_tests::pep_diagnostics_exclude_payloads_and_identifiers ... ok
|
||||
test http_service::tests::upload_claim_refreshes_completion_state_after_an_earlier_snapshot ... ok
|
||||
test sessions::push_timeout_tests::first_important_push_shortens_extended_hibernation_once ... ok
|
||||
test sessions::tests::acknowledgement_wraps ... ok
|
||||
test sessions::tests::graceful_disconnect_is_not_resumable ... ok
|
||||
test sessions::tests::resume_replays_only_unacked_and_checks_owner ... ok
|
||||
test sessions::tests::traffic_before_sm_enable_is_not_counted ... ok
|
||||
test upload_migration::tests::local_spool_checks_size_and_hash_and_preserves_source ... ok
|
||||
test sasl::tests::sha256_independent_python_vector ... ok
|
||||
test wire::tests::arbitrary_prefix_and_inherited_namespaces ... ok
|
||||
test wire::tests::cdata_and_comments_frame_without_counting_brackets ... ok
|
||||
test wire::tests::declarations_rejected ... ok
|
||||
test wire::tests::fragmented_and_pipelined ... ok
|
||||
test wire::tests::invalid_namespace_and_declarations_rejected ... ok
|
||||
test upload_migration::tests::pending_and_corrupt_journals_block_startup ... ok
|
||||
test http_service::tests::upload_internal_mount_is_independent_of_advertised_proxy_prefix ... ok
|
||||
test http_service::tests::abandoned_slots_release_quota_without_pruning_existing_or_active_files ... ok
|
||||
test sasl::tests::plus_channel_binding_and_ssdp_are_authenticated ... ok
|
||||
test server::authorization_tests::upload_requires_registered_local_account_or_explicit_remote_access ... ok
|
||||
test sasl::tests::wrong_proof_nonce_binding_and_duplicates ... ok
|
||||
test admin::tests::real_admin_xml_protocol_over_duplex ... ok
|
||||
|
||||
test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.18s
|
||||
|
||||
Running tests/archive_edges.rs (target/debug/deps/archive_edges-da556b3567110d0b)
|
||||
|
||||
running 6 tests
|
||||
test missing_empty_whitespace_and_single_record ... ok
|
||||
test endpoint_read_recovers_partial_append_before_seeking ... ok
|
||||
test multiline_endpoints_match_native_scanner_with_blank_lines ... ok
|
||||
test oversized_prefix_and_malformed_endpoints_fail ... ok
|
||||
test metadata_skips_large_malformed_middle_without_creating_cache ... ok
|
||||
test tail_padding_respects_record_bound ... ok
|
||||
|
||||
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.26s
|
||||
|
||||
Running tests/archive_redo.rs (target/debug/deps/archive_redo-d041d27370ba29bd)
|
||||
|
||||
running 13 tests
|
||||
test atomic_native_replace_without_handoff_cannot_discard_committed_frames ... ok
|
||||
test explicit_native_atomic_handoff_and_missing_baseline_fail_closed ... ok
|
||||
|
||||
running 1 test
|
||||
test durable_handoff_allows_native_in_place_append_and_next_rust_append ... ok
|
||||
test legacy_rollback_marker_is_migrated_before_redo_append ... ok
|
||||
test committed_redo_recovers_missing_and_partial_native_without_duplicates ... ok
|
||||
test redo_process_child ... ok
|
||||
test killed_writer_retains_accepted_records_after_native_data_loss ... ok
|
||||
test native_baseline_in_place_edits_are_rejected ... ok
|
||||
test complete_checksum_corruption_fails_without_modifying_native_or_journal ... ok
|
||||
test copied_archive_replays_committed_redo_despite_new_native_inode ... ok
|
||||
test incomplete_final_frame_and_uncommitted_native_suffix_are_discarded ... ok
|
||||
test byte_limit_rotates_before_frame_count_limit ... ok
|
||||
test bounded_rotation_recovers_suffix_and_rewrites_never_resurrect_deleted_records ... ok
|
||||
|
||||
test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.91s
|
||||
|
||||
Running tests/sm_journal.rs (target/debug/deps/sm_journal-79fad6209c2b84bb)
|
||||
|
||||
running 14 tests
|
||||
test offline_checkpoint_refuses_live_store_and_preserves_redo ... ok
|
||||
test complete_corrupt_frame_fails_closed_even_with_valid_export ... ok
|
||||
test journal_symlink_is_rejected_without_following_target ... ok
|
||||
test export_failure_after_commit_advances_memory_and_fences_later_writes ... ok
|
||||
test durable_tombstone_overrides_resurrected_stale_json ... ok
|
||||
test offline_checkpoint_durably_applies_expiration_and_tombstones ... ok
|
||||
test legacy_json_migrates_and_baseline_survives_partial_first_append ... ok
|
||||
test offline_checkpoint_repairs_exports_removes_redo_and_allows_legacy_restart ... ok
|
||||
test partial_tail_is_discarded_and_subsequent_commits_survive_restart ... ok
|
||||
test redo_repairs_missing_or_torn_export_before_json_validation ... ok
|
||||
test superseded_frames_still_require_valid_checksums_json_and_baseline_flags ... ok
|
||||
test incomplete_baseline_or_magic_never_falls_back_to_json ... ok
|
||||
test retired_journals_are_collected_in_bounded_batches ... ok
|
||||
test rotation_preserves_latest_snapshot_and_bounds_redo_size ... ok
|
||||
|
||||
test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.97s
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"scope": "loopback STARTTLS; four SM resources/two accounts; MAM + pending avatar metadata PEP; fixture-only fsync/fdatasync including data directories; real sync retained; operation windows include background SM heartbeat",
|
||||
"cases": [
|
||||
{
|
||||
"samples": [
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 18.615095,
|
||||
"sent": 19.940848,
|
||||
"received": 26.953392
|
||||
},
|
||||
"sender_sm_handled_ms": 26.88493,
|
||||
"publication_plus_delivery_ms": 27.065981,
|
||||
"sync": {
|
||||
"total": 12,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 2,
|
||||
"mam:file:fsync": 6,
|
||||
"sm:file:fdatasync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 1,
|
||||
"delivery_ms": {
|
||||
"direct": 6.003256,
|
||||
"sent": 7.461811,
|
||||
"received": 10.135311
|
||||
},
|
||||
"sender_sm_handled_ms": 9.993133,
|
||||
"publication_plus_delivery_ms": 10.173245,
|
||||
"sync": {
|
||||
"total": 6,
|
||||
"by_store_kind_op": {
|
||||
"mam:file:fsync": 2,
|
||||
"sm:file:fdatasync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 2,
|
||||
"delivery_ms": {
|
||||
"direct": 7.871244,
|
||||
"sent": 13.331419,
|
||||
"received": 15.325955
|
||||
},
|
||||
"sender_sm_handled_ms": 15.079213,
|
||||
"publication_plus_delivery_ms": 15.363148,
|
||||
"sync": {
|
||||
"total": 6,
|
||||
"by_store_kind_op": {
|
||||
"mam:file:fsync": 2,
|
||||
"sm:file:fdatasync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 38.754553,
|
||||
"sent": 25.865646,
|
||||
"received": 40.65703
|
||||
},
|
||||
"sender_sm_handled_ms": 40.40549,
|
||||
"publication_plus_delivery_ms": 40.769686,
|
||||
"sync": {
|
||||
"total": 14,
|
||||
"by_store_kind_op": {
|
||||
"mam:file:fsync": 2,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:file:fdatasync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 1,
|
||||
"delivery_ms": {
|
||||
"direct": 20.942607,
|
||||
"sent": 20.204899,
|
||||
"received": 22.05411
|
||||
},
|
||||
"sender_sm_handled_ms": 21.856674,
|
||||
"publication_plus_delivery_ms": 23.679041,
|
||||
"sync": {
|
||||
"total": 14,
|
||||
"by_store_kind_op": {
|
||||
"mam:file:fsync": 2,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:file:fdatasync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 2,
|
||||
"delivery_ms": {
|
||||
"direct": 17.980156,
|
||||
"sent": 18.759152,
|
||||
"received": 21.369599
|
||||
},
|
||||
"sender_sm_handled_ms": 21.332607,
|
||||
"publication_plus_delivery_ms": 21.518131,
|
||||
"sync": {
|
||||
"total": 14,
|
||||
"by_store_kind_op": {
|
||||
"mam:file:fsync": 2,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:file:fdatasync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"mam_records_per_account": [
|
||||
106,
|
||||
106
|
||||
],
|
||||
"status": "pass",
|
||||
"delay_ms_per_sync": 0,
|
||||
"server_message_processing_slow_events": [],
|
||||
"server_message_processing_slow_ms": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"binary_sha256": "130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5",
|
||||
"scope": "loopback STARTTLS; four SM resources/two accounts; MAM + pending avatar metadata PEP; fixture-only fsync/fdatasync including data directories; real sync retained; operation windows include background SM heartbeat",
|
||||
"cases": [
|
||||
{
|
||||
"samples": [
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 26.330895,
|
||||
"sent": 29.575091,
|
||||
"received": 33.552895
|
||||
},
|
||||
"sender_sm_handled_ms": 33.407559,
|
||||
"publication_plus_delivery_ms": 33.592558,
|
||||
"sync": {
|
||||
"total": 16,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"sm:directory:fsync": 4,
|
||||
"sm:file:fsync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 1,
|
||||
"delivery_ms": {
|
||||
"direct": 25.112248,
|
||||
"sent": 31.688103,
|
||||
"received": 39.725842
|
||||
},
|
||||
"sender_sm_handled_ms": 39.683719,
|
||||
"publication_plus_delivery_ms": 39.867826,
|
||||
"sync": {
|
||||
"total": 16,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"sm:directory:fsync": 4,
|
||||
"sm:file:fsync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "settled",
|
||||
"round": 2,
|
||||
"delivery_ms": {
|
||||
"direct": 11.894093,
|
||||
"sent": 11.977544,
|
||||
"received": 13.638711
|
||||
},
|
||||
"sender_sm_handled_ms": 13.371964,
|
||||
"publication_plus_delivery_ms": 13.720665,
|
||||
"sync": {
|
||||
"total": 16,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"sm:directory:fsync": 4,
|
||||
"sm:file:fsync": 4
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 0,
|
||||
"delivery_ms": {
|
||||
"direct": 42.768972,
|
||||
"sent": 32.889933,
|
||||
"received": 48.207819
|
||||
},
|
||||
"sender_sm_handled_ms": 47.576405,
|
||||
"publication_plus_delivery_ms": 51.552818,
|
||||
"sync": {
|
||||
"total": 28,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:directory:fsync": 8,
|
||||
"sm:file:fsync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 1,
|
||||
"delivery_ms": {
|
||||
"direct": 28.176101,
|
||||
"sent": 26.386914,
|
||||
"received": 29.822606
|
||||
},
|
||||
"sender_sm_handled_ms": 29.714999,
|
||||
"publication_plus_delivery_ms": 30.011235,
|
||||
"sync": {
|
||||
"total": 28,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:directory:fsync": 8,
|
||||
"sm:file:fsync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "avatar_pending",
|
||||
"round": 2,
|
||||
"delivery_ms": {
|
||||
"direct": 29.714366,
|
||||
"sent": 26.01394,
|
||||
"received": 32.367508
|
||||
},
|
||||
"sender_sm_handled_ms": 32.252144,
|
||||
"publication_plus_delivery_ms": 33.076522,
|
||||
"sync": {
|
||||
"total": 28,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 4,
|
||||
"mam:file:fsync": 4,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:directory:fsync": 8,
|
||||
"sm:file:fsync": 8
|
||||
}
|
||||
},
|
||||
"pending_per_resource": [
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"mam_records_per_account": [
|
||||
106,
|
||||
106
|
||||
],
|
||||
"status": "pass",
|
||||
"delay_ms_per_sync": 0,
|
||||
"server_message_processing_slow_events": [],
|
||||
"server_message_processing_slow_ms": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"version": "0.7.9",
|
||||
"date_utc": "2026-10-20",
|
||||
"ordinary_tests": {
|
||||
"passed": 265,
|
||||
"failed": 0,
|
||||
"ignored": 11
|
||||
},
|
||||
"fmt": "passed",
|
||||
"clippy": "passed",
|
||||
"release": "passed",
|
||||
"native_module_tests": 8,
|
||||
"native_federation_tests": 1,
|
||||
"release_binary_sha256": "24c0dca637e7ce4e238ef6becad16bb5a18a33546554b0eecf310fd8927f021c",
|
||||
"upload": {
|
||||
"minio_10gib": {
|
||||
"ok": true,
|
||||
"minio_version": "minio-sandbox-compat version DEVELOPMENT.GOGET (commit-id=DEVELOPMENT.GOGET)\nRuntime: go1.24.3 linux/amd64\nLicense: GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html\nCopyright: 2015-0000 MinIO, Inc.",
|
||||
"minio_binary_sha256": "8bbb9bccfbac69869ab499c39cfd8731b2b770b86ccf37cca1c8267556ad9bf0",
|
||||
"binary": "/workspace/scratch/362577cdf2e0/recovered/rust-xmpp/target/release/rust-xmpp",
|
||||
"binary_version": "rust-xmpp 0.7.9 — experimental XMPP runtime",
|
||||
"binary_sha256": "24c0dca637e7ce4e238ef6becad16bb5a18a33546554b0eecf310fd8927f021c",
|
||||
"size": 10737418240,
|
||||
"qualifies_over_5gib": true,
|
||||
"minio_test_only_compatibility_patch": {
|
||||
"sha256": "e60592a65273f28a1375eb6ecbc0966f7fbd2ba7567d2ffedc13ffd95ed224f5",
|
||||
"diff": "--- a/cmd/net.go\n+++ b/cmd/net.go\n@@ -24,6 +24,7 @@\n \t\"runtime\"\n \t\"sort\"\n \t\"strings\"\n+\t\"syscall\"\n \n \t\"github.com/minio/minio-go/v7/pkg/set\"\n \t\"github.com/minio/minio/internal/config\"\n@@ -54,6 +55,10 @@\n // mustGetLocalIPs returns IPs of local interface\n func mustGetLocalIPs() (ipList []net.IP) {\n \tifs, err := net.Interfaces()\n+\t// TEST ONLY: the sandbox denies netlink enumeration; all listeners bind loopback.\n+\tif errors.Is(err, syscall.EPERM) {\n+\t\treturn []net.IP{net.ParseIP(\"127.0.0.1\")}\n+\t}\n \tlogger.FatalIf(err, \"Unable to get IP addresses of this host\")\n \n \tfor _, interf := range ifs {\n",
|
||||
"limitation": "Modified upstream MinIO: EPERM interface enumeration uses loopback; all S3/auth/storage code remains upstream."
|
||||
},
|
||||
"max_rss_bytes": {
|
||||
"server": 106057728,
|
||||
"client": 24338432,
|
||||
"minio": 498958336
|
||||
},
|
||||
"put_bytes": 10737418240,
|
||||
"put_seconds": 48.45253416699961,
|
||||
"sha256": "e4c296c41d3a9605f0348961e41ed835b5f42c220751ed2269a726951546c935",
|
||||
"minio_etag": "958f42d583ac40ff209eb865c5eb4bd6-1280",
|
||||
"multipart_parts": 1280,
|
||||
"minio_allocated_bytes": 10747953152,
|
||||
"free_disk_after_put_bytes": 15180328960,
|
||||
"get_bytes": 10737418240,
|
||||
"get_sha256": "e4c296c41d3a9605f0348961e41ed835b5f42c220751ed2269a726951546c935",
|
||||
"get_seconds": 13.709708632000911,
|
||||
"range_offsets": [
|
||||
0,
|
||||
4294967289,
|
||||
5368709113,
|
||||
10737418208
|
||||
],
|
||||
"restart_range_ok": true,
|
||||
"restart_reuse_status": 409,
|
||||
"crash_multipart_cleanup_ok": true,
|
||||
"minio_upstream_provenance": {
|
||||
"source_url": "https://github.com/minio/minio/archive/refs/tags/RELEASE.2025-04-22T22-12-26Z.tar.gz",
|
||||
"upstream_release": "RELEASE.2025-04-22T22-12-26Z",
|
||||
"build": "Go1.24.3 go build -p4; no dependency changes",
|
||||
"sha256": {
|
||||
"/tmp/minio-source.tar.gz": "1a63209927b9d1fb239b2a776b7b01d1902eff19379a108f4bc7005edcc0968c",
|
||||
"/tmp/minio-genuine": "3dd5d6756ad17c599fbc1c6f6a2f5b590242c049522e848c700949dbf8e60ad9",
|
||||
"/tmp/minio-sandbox-compat": "8bbb9bccfbac69869ab499c39cfd8731b2b770b86ccf37cca1c8267556ad9bf0",
|
||||
"/tmp/minio-sandbox-compat.patch": "e60592a65273f28a1375eb6ecbc0966f7fbd2ba7567d2ffedc13ffd95ed224f5"
|
||||
},
|
||||
"compatibility_limitation": "Only net.Interfaces EPERM becomes loopback127.0.0.1; all S3/auth/multipart/storage source unchanged."
|
||||
}
|
||||
},
|
||||
"migration_network": "passed: local -> S3 A -> S3 B -> local, dry-run, lock, native metadata/source preservation, interrupted copy/resume, native final file with incomplete cache",
|
||||
"s3_fault_network": "passed: SigV4, paginated exact-key orphan cleanup, foreign prefix safety, missing UploadId crash recovery, conditional Complete, ambiguous completion quarantine and late recovery",
|
||||
"claim_race_regression": "passed: atomic fresh snapshot and active claim retains latest completion/quarantine flags"
|
||||
},
|
||||
"load": {
|
||||
"total_load_wall_seconds": 7.094,
|
||||
"requested_resources": 500,
|
||||
"authenticated_bound_resources": 500,
|
||||
"connection_setup_concurrency": 50,
|
||||
"setup_wall_seconds": 0.671,
|
||||
"setup_latency": {
|
||||
"samples": 500,
|
||||
"median_ms": 62.192,
|
||||
"p95_ms": 83.883,
|
||||
"max_ms": 114.563
|
||||
},
|
||||
"ping_rounds": 10,
|
||||
"ping_round_trip": {
|
||||
"samples": 5000,
|
||||
"median_ms": 13.502,
|
||||
"p95_ms": 19.31,
|
||||
"max_ms": 24.054
|
||||
},
|
||||
"accounts": 100,
|
||||
"message_rounds": 10,
|
||||
"messages_sent": 5000,
|
||||
"messages_received": 5000,
|
||||
"message_batch_seconds": 4.134,
|
||||
"metrics_before": {
|
||||
"rss_kib": 8048,
|
||||
"cpu_seconds": 0.0,
|
||||
"fd_count": 11
|
||||
},
|
||||
"metrics_connected": {
|
||||
"rss_kib": 26804,
|
||||
"cpu_seconds": 1.02,
|
||||
"fd_count": 511
|
||||
},
|
||||
"metrics_after_workload": {
|
||||
"rss_kib": 27400,
|
||||
"cpu_seconds": 8.85,
|
||||
"fd_count": 511
|
||||
},
|
||||
"metrics_two_seconds_after_disconnect": {
|
||||
"rss_kib": 27464,
|
||||
"cpu_seconds": 8.9,
|
||||
"fd_count": 11
|
||||
},
|
||||
"sampled_peak_rss_kib": 27464,
|
||||
"fd_delta_after_disconnect": 0,
|
||||
"failures": [],
|
||||
"scope": "One loopback host, 100 accounts with distinct resources, TLS+PLAIN, ping and directed messages; not a production benchmark or proof of no memory leaks.",
|
||||
"mam": {
|
||||
"archive_files": 100,
|
||||
"records": 10000,
|
||||
"expected_records": 10000
|
||||
}
|
||||
},
|
||||
"limitations": [
|
||||
"MinIO upstream RELEASE.2025-04-22T22-12-26Z with disclosed five-line EPERM loopback interface enumeration adapter; S3/auth/storage/multipart code unchanged",
|
||||
"Single-node loopback MinIO, HTTP gateway-to-store and verified HTTPS client-to-gateway; no AWS account or distributed/production storage qualification",
|
||||
"Missing/corrupt ownership metadata cannot identify arbitrary bucket orphans; lifecycle remains defense in depth",
|
||||
"Ambiguous completion with HEAD404 quarantines slot until a trustworthy remote outcome; no automatic destructive reset",
|
||||
"Migration is offline, source preserving, requires temporary disk up to one largest file; no live migration",
|
||||
"Previous full Prosody parity / production client / DNSSEC limits unchanged"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"version": "0.8.3",
|
||||
"date_utc": "2026-10-21",
|
||||
"release_binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"ordinary_tests": {
|
||||
"passed": 334,
|
||||
"failed": 0,
|
||||
"ignored": 11
|
||||
},
|
||||
"fmt": "passed",
|
||||
"clippy": "passed",
|
||||
"release": "passed",
|
||||
"full_verify": "passed in one final frozen-source run",
|
||||
"native_module_tests": 8,
|
||||
"native_federation_tests": 1,
|
||||
"extra_large_archive_tests": 1,
|
||||
"ignored_not_run": [
|
||||
"AF_UNIX admin_socket"
|
||||
],
|
||||
"load": {
|
||||
"general_tls_clients": 500,
|
||||
"general_accounts": 100,
|
||||
"muc_sm": {
|
||||
"clients": 500,
|
||||
"accounts": 100,
|
||||
"rooms": 10,
|
||||
"message_rounds": 3,
|
||||
"concurrency": 20,
|
||||
"binary": "/tmp/xmpp-durable-muc-load-ssixqqk9/rust-xmpp",
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"before": {
|
||||
"rss_kib": 9516,
|
||||
"cpu_seconds": 0.01,
|
||||
"fd_count": 12
|
||||
},
|
||||
"connect_seconds": 12.048,
|
||||
"connect_latency": {
|
||||
"samples": 500,
|
||||
"median_ms": 398.044,
|
||||
"p95_ms": 1086.236,
|
||||
"max_ms": 1403.7
|
||||
},
|
||||
"phase": "completed",
|
||||
"status": "pass",
|
||||
"elapsed_seconds": 128.38,
|
||||
"join_seconds": 94.493,
|
||||
"joined": {
|
||||
"rss_kib": 41828,
|
||||
"cpu_seconds": 55.61,
|
||||
"fd_count": 513
|
||||
},
|
||||
"join_presence_deliveries": 25000,
|
||||
"slots_joined": {
|
||||
"count": 500,
|
||||
"bytes": 309790
|
||||
},
|
||||
"message_seconds": 9.585,
|
||||
"room_messages_sent": 30,
|
||||
"room_messages_delivered": 1500,
|
||||
"before_kill": {
|
||||
"rss_kib": 41740,
|
||||
"cpu_seconds": 62.6,
|
||||
"fd_count": 512
|
||||
},
|
||||
"slots_before_kill": {
|
||||
"count": 500,
|
||||
"bytes": 514030
|
||||
},
|
||||
"restart_seconds": 7.557,
|
||||
"resumed_resources": 10,
|
||||
"replayed_pending_room_messages": 10,
|
||||
"membership_groupchat_deliveries_after_resume": 10,
|
||||
"after_resume": {
|
||||
"rss_kib": 20896,
|
||||
"cpu_seconds": 7.09,
|
||||
"fd_count": 22
|
||||
},
|
||||
"slots_after_resume": {
|
||||
"count": 500,
|
||||
"bytes": 671070
|
||||
},
|
||||
"total_seconds": 128.38,
|
||||
"peak_rss_kib": 41852
|
||||
}
|
||||
},
|
||||
"personal_delivery_comparisons": [
|
||||
{
|
||||
"delay_ms_per_sync": 0,
|
||||
"phase": "settled",
|
||||
"samples_per_binary": 3,
|
||||
"body_before_ms": 25.112248,
|
||||
"body_after_ms": 7.871244,
|
||||
"sender_ack_before_ms": 33.407559,
|
||||
"sender_ack_after_ms": 15.079213
|
||||
},
|
||||
{
|
||||
"delay_ms_per_sync": 0,
|
||||
"phase": "avatar_pending",
|
||||
"samples_per_binary": 3,
|
||||
"body_before_ms": 29.714366,
|
||||
"body_after_ms": 20.942607,
|
||||
"sender_ack_before_ms": 32.252144,
|
||||
"sender_ack_after_ms": 21.856674
|
||||
},
|
||||
{
|
||||
"delay_ms_per_sync": 100,
|
||||
"phase": "settled",
|
||||
"samples_per_binary": 3,
|
||||
"body_before_ms": 1232.468024,
|
||||
"body_after_ms": 419.683064,
|
||||
"sender_ack_before_ms": 1641.598264,
|
||||
"sender_ack_after_ms": 644.082891
|
||||
},
|
||||
{
|
||||
"delay_ms_per_sync": 100,
|
||||
"phase": "avatar_pending",
|
||||
"samples_per_binary": 3,
|
||||
"body_before_ms": 2683.292251,
|
||||
"body_after_ms": 1378.231158,
|
||||
"sender_ack_before_ms": 3195.209226,
|
||||
"sender_ack_after_ms": 1581.975429
|
||||
},
|
||||
{
|
||||
"delay_ms_per_sync": 200,
|
||||
"phase": "settled",
|
||||
"samples_per_binary": 1,
|
||||
"body_before_ms": 2836.63993,
|
||||
"body_after_ms": 2042.56345,
|
||||
"sender_ack_before_ms": 3643.297905,
|
||||
"sender_ack_after_ms": 2441.985416
|
||||
},
|
||||
{
|
||||
"delay_ms_per_sync": 200,
|
||||
"phase": "avatar_pending",
|
||||
"samples_per_binary": 1,
|
||||
"body_before_ms": 5782.018326,
|
||||
"body_after_ms": 2676.283903,
|
||||
"sender_ack_before_ms": 6595.924675,
|
||||
"sender_ack_after_ms": 3094.949323
|
||||
}
|
||||
],
|
||||
"durability_crash": {
|
||||
"status": "pass",
|
||||
"delay_ms_per_sync": 100,
|
||||
"acknowledged_messages": 3,
|
||||
"mam_records_per_account": [
|
||||
3,
|
||||
3
|
||||
],
|
||||
"resumed_resources": 4,
|
||||
"replayed_personal_deliveries": 9,
|
||||
"offline_checkpoint": {
|
||||
"exported_sessions": 4,
|
||||
"native_archives": 2,
|
||||
"journals_removed": true,
|
||||
"second_invocation_idempotent": true
|
||||
},
|
||||
"pending_before_reauthentication": [
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
3
|
||||
],
|
||||
"sync_between_publication_and_sigkill": {
|
||||
"total": 32,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 2,
|
||||
"mam:file:fsync": 10,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:file:fdatasync": 16
|
||||
}
|
||||
},
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"scope": "loopback STARTTLS; real delayed fsync/fdatasync; sender SM acknowledgement then SIGKILL; authoritative queues and MAM checked before reauthentication; at-least-once replay"
|
||||
},
|
||||
"recovery_sync_faults": {
|
||||
"status": "pass",
|
||||
"cases": [
|
||||
{
|
||||
"suffix": ".wal",
|
||||
"status": "pass",
|
||||
"checkpoint_rejected_sync_failure": true,
|
||||
"journals_preserved_on_failure": 4,
|
||||
"serve_rejected_before_listeners": true,
|
||||
"clean_retry_preserved_sessions": 4,
|
||||
"clean_retry_preserved_archives": 2
|
||||
},
|
||||
{
|
||||
"suffix": ".rredo",
|
||||
"status": "pass",
|
||||
"checkpoint_rejected_sync_failure": true,
|
||||
"journals_preserved_on_failure": 2,
|
||||
"serve_rejected_before_listeners": false,
|
||||
"clean_retry_preserved_sessions": 4,
|
||||
"clean_retry_preserved_archives": 2
|
||||
}
|
||||
],
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a"
|
||||
},
|
||||
"limitations": [
|
||||
"Loopback fixture, not production client or disk qualification",
|
||||
"0ms pair ran sequentially without full verify; 100/200ms final runs had background final verify",
|
||||
"One200ms sample; startup/migration/rotation and background heartbeat affect sync counts",
|
||||
"SM global IO mutex and full snapshots remain; no atomic transaction spanning MAM/SM/network",
|
||||
"Offline storage-checkpoint required before legacy/native writer handoff",
|
||||
"10GiB transfers, real MinIO/AWS, Docker/Gitea/GitHub run were not repeated in this release"
|
||||
]
|
||||
}
|
||||
+193
-117
@@ -1,133 +1,209 @@
|
||||
{
|
||||
"version": "0.7.9",
|
||||
"date_utc": "2026-10-20",
|
||||
"version": "0.8.3",
|
||||
"date_utc": "2026-10-21",
|
||||
"release_binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"ordinary_tests": {
|
||||
"passed": 265,
|
||||
"passed": 334,
|
||||
"failed": 0,
|
||||
"ignored": 11
|
||||
},
|
||||
"fmt": "passed",
|
||||
"clippy": "passed",
|
||||
"release": "passed",
|
||||
"full_verify": "passed in one final frozen-source run",
|
||||
"native_module_tests": 8,
|
||||
"native_federation_tests": 1,
|
||||
"release_binary_sha256": "24c0dca637e7ce4e238ef6becad16bb5a18a33546554b0eecf310fd8927f021c",
|
||||
"upload": {
|
||||
"minio_10gib": {
|
||||
"ok": true,
|
||||
"minio_version": "minio-sandbox-compat version DEVELOPMENT.GOGET (commit-id=DEVELOPMENT.GOGET)\nRuntime: go1.24.3 linux/amd64\nLicense: GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html\nCopyright: 2015-0000 MinIO, Inc.",
|
||||
"minio_binary_sha256": "8bbb9bccfbac69869ab499c39cfd8731b2b770b86ccf37cca1c8267556ad9bf0",
|
||||
"binary": "/workspace/scratch/362577cdf2e0/recovered/rust-xmpp/target/release/rust-xmpp",
|
||||
"binary_version": "rust-xmpp 0.7.9 — experimental XMPP runtime",
|
||||
"binary_sha256": "24c0dca637e7ce4e238ef6becad16bb5a18a33546554b0eecf310fd8927f021c",
|
||||
"size": 10737418240,
|
||||
"qualifies_over_5gib": true,
|
||||
"minio_test_only_compatibility_patch": {
|
||||
"sha256": "e60592a65273f28a1375eb6ecbc0966f7fbd2ba7567d2ffedc13ffd95ed224f5",
|
||||
"diff": "--- a/cmd/net.go\n+++ b/cmd/net.go\n@@ -24,6 +24,7 @@\n \t\"runtime\"\n \t\"sort\"\n \t\"strings\"\n+\t\"syscall\"\n \n \t\"github.com/minio/minio-go/v7/pkg/set\"\n \t\"github.com/minio/minio/internal/config\"\n@@ -54,6 +55,10 @@\n // mustGetLocalIPs returns IPs of local interface\n func mustGetLocalIPs() (ipList []net.IP) {\n \tifs, err := net.Interfaces()\n+\t// TEST ONLY: the sandbox denies netlink enumeration; all listeners bind loopback.\n+\tif errors.Is(err, syscall.EPERM) {\n+\t\treturn []net.IP{net.ParseIP(\"127.0.0.1\")}\n+\t}\n \tlogger.FatalIf(err, \"Unable to get IP addresses of this host\")\n \n \tfor _, interf := range ifs {\n",
|
||||
"limitation": "Modified upstream MinIO: EPERM interface enumeration uses loopback; all S3/auth/storage code remains upstream."
|
||||
},
|
||||
"max_rss_bytes": {
|
||||
"server": 106057728,
|
||||
"client": 24338432,
|
||||
"minio": 498958336
|
||||
},
|
||||
"put_bytes": 10737418240,
|
||||
"put_seconds": 48.45253416699961,
|
||||
"sha256": "e4c296c41d3a9605f0348961e41ed835b5f42c220751ed2269a726951546c935",
|
||||
"minio_etag": "958f42d583ac40ff209eb865c5eb4bd6-1280",
|
||||
"multipart_parts": 1280,
|
||||
"minio_allocated_bytes": 10747953152,
|
||||
"free_disk_after_put_bytes": 15180328960,
|
||||
"get_bytes": 10737418240,
|
||||
"get_sha256": "e4c296c41d3a9605f0348961e41ed835b5f42c220751ed2269a726951546c935",
|
||||
"get_seconds": 13.709708632000911,
|
||||
"range_offsets": [
|
||||
0,
|
||||
4294967289,
|
||||
5368709113,
|
||||
10737418208
|
||||
],
|
||||
"restart_range_ok": true,
|
||||
"restart_reuse_status": 409,
|
||||
"crash_multipart_cleanup_ok": true,
|
||||
"minio_upstream_provenance": {
|
||||
"source_url": "https://github.com/minio/minio/archive/refs/tags/RELEASE.2025-04-22T22-12-26Z.tar.gz",
|
||||
"upstream_release": "RELEASE.2025-04-22T22-12-26Z",
|
||||
"build": "Go1.24.3 go build -p4; no dependency changes",
|
||||
"sha256": {
|
||||
"/tmp/minio-source.tar.gz": "1a63209927b9d1fb239b2a776b7b01d1902eff19379a108f4bc7005edcc0968c",
|
||||
"/tmp/minio-genuine": "3dd5d6756ad17c599fbc1c6f6a2f5b590242c049522e848c700949dbf8e60ad9",
|
||||
"/tmp/minio-sandbox-compat": "8bbb9bccfbac69869ab499c39cfd8731b2b770b86ccf37cca1c8267556ad9bf0",
|
||||
"/tmp/minio-sandbox-compat.patch": "e60592a65273f28a1375eb6ecbc0966f7fbd2ba7567d2ffedc13ffd95ed224f5"
|
||||
},
|
||||
"compatibility_limitation": "Only net.Interfaces EPERM becomes loopback127.0.0.1; all S3/auth/multipart/storage source unchanged."
|
||||
}
|
||||
},
|
||||
"migration_network": "passed: local -> S3 A -> S3 B -> local, dry-run, lock, native metadata/source preservation, interrupted copy/resume, native final file with incomplete cache",
|
||||
"s3_fault_network": "passed: SigV4, paginated exact-key orphan cleanup, foreign prefix safety, missing UploadId crash recovery, conditional Complete, ambiguous completion quarantine and late recovery",
|
||||
"claim_race_regression": "passed: atomic fresh snapshot and active claim retains latest completion/quarantine flags"
|
||||
},
|
||||
"extra_large_archive_tests": 1,
|
||||
"ignored_not_run": [
|
||||
"AF_UNIX admin_socket"
|
||||
],
|
||||
"load": {
|
||||
"total_load_wall_seconds": 7.094,
|
||||
"requested_resources": 500,
|
||||
"authenticated_bound_resources": 500,
|
||||
"connection_setup_concurrency": 50,
|
||||
"setup_wall_seconds": 0.671,
|
||||
"setup_latency": {
|
||||
"samples": 500,
|
||||
"median_ms": 62.192,
|
||||
"p95_ms": 83.883,
|
||||
"max_ms": 114.563
|
||||
},
|
||||
"ping_rounds": 10,
|
||||
"ping_round_trip": {
|
||||
"samples": 5000,
|
||||
"median_ms": 13.502,
|
||||
"p95_ms": 19.31,
|
||||
"max_ms": 24.054
|
||||
},
|
||||
"accounts": 100,
|
||||
"message_rounds": 10,
|
||||
"messages_sent": 5000,
|
||||
"messages_received": 5000,
|
||||
"message_batch_seconds": 4.134,
|
||||
"metrics_before": {
|
||||
"rss_kib": 8048,
|
||||
"cpu_seconds": 0.0,
|
||||
"fd_count": 11
|
||||
},
|
||||
"metrics_connected": {
|
||||
"rss_kib": 26804,
|
||||
"cpu_seconds": 1.02,
|
||||
"fd_count": 511
|
||||
},
|
||||
"metrics_after_workload": {
|
||||
"rss_kib": 27400,
|
||||
"cpu_seconds": 8.85,
|
||||
"fd_count": 511
|
||||
},
|
||||
"metrics_two_seconds_after_disconnect": {
|
||||
"rss_kib": 27464,
|
||||
"cpu_seconds": 8.9,
|
||||
"fd_count": 11
|
||||
},
|
||||
"sampled_peak_rss_kib": 27464,
|
||||
"fd_delta_after_disconnect": 0,
|
||||
"failures": [],
|
||||
"scope": "One loopback host, 100 accounts with distinct resources, TLS+PLAIN, ping and directed messages; not a production benchmark or proof of no memory leaks.",
|
||||
"mam": {
|
||||
"archive_files": 100,
|
||||
"records": 10000,
|
||||
"expected_records": 10000
|
||||
"general_tls_clients": 500,
|
||||
"general_accounts": 100,
|
||||
"muc_sm": {
|
||||
"clients": 500,
|
||||
"accounts": 100,
|
||||
"rooms": 10,
|
||||
"message_rounds": 3,
|
||||
"concurrency": 20,
|
||||
"binary": "/tmp/xmpp-durable-muc-load-ssixqqk9/rust-xmpp",
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"before": {
|
||||
"rss_kib": 9516,
|
||||
"cpu_seconds": 0.01,
|
||||
"fd_count": 12
|
||||
},
|
||||
"connect_seconds": 12.048,
|
||||
"connect_latency": {
|
||||
"samples": 500,
|
||||
"median_ms": 398.044,
|
||||
"p95_ms": 1086.236,
|
||||
"max_ms": 1403.7
|
||||
},
|
||||
"phase": "completed",
|
||||
"status": "pass",
|
||||
"elapsed_seconds": 128.38,
|
||||
"join_seconds": 94.493,
|
||||
"joined": {
|
||||
"rss_kib": 41828,
|
||||
"cpu_seconds": 55.61,
|
||||
"fd_count": 513
|
||||
},
|
||||
"join_presence_deliveries": 25000,
|
||||
"slots_joined": {
|
||||
"count": 500,
|
||||
"bytes": 309790
|
||||
},
|
||||
"message_seconds": 9.585,
|
||||
"room_messages_sent": 30,
|
||||
"room_messages_delivered": 1500,
|
||||
"before_kill": {
|
||||
"rss_kib": 41740,
|
||||
"cpu_seconds": 62.6,
|
||||
"fd_count": 512
|
||||
},
|
||||
"slots_before_kill": {
|
||||
"count": 500,
|
||||
"bytes": 514030
|
||||
},
|
||||
"restart_seconds": 7.557,
|
||||
"resumed_resources": 10,
|
||||
"replayed_pending_room_messages": 10,
|
||||
"membership_groupchat_deliveries_after_resume": 10,
|
||||
"after_resume": {
|
||||
"rss_kib": 20896,
|
||||
"cpu_seconds": 7.09,
|
||||
"fd_count": 22
|
||||
},
|
||||
"slots_after_resume": {
|
||||
"count": 500,
|
||||
"bytes": 671070
|
||||
},
|
||||
"total_seconds": 128.38,
|
||||
"peak_rss_kib": 41852
|
||||
}
|
||||
},
|
||||
"personal_delivery_comparisons": [
|
||||
{
|
||||
"delay_ms_per_sync": 0,
|
||||
"phase": "settled",
|
||||
"samples_per_binary": 3,
|
||||
"body_before_ms": 25.112248,
|
||||
"body_after_ms": 7.871244,
|
||||
"sender_ack_before_ms": 33.407559,
|
||||
"sender_ack_after_ms": 15.079213
|
||||
},
|
||||
{
|
||||
"delay_ms_per_sync": 0,
|
||||
"phase": "avatar_pending",
|
||||
"samples_per_binary": 3,
|
||||
"body_before_ms": 29.714366,
|
||||
"body_after_ms": 20.942607,
|
||||
"sender_ack_before_ms": 32.252144,
|
||||
"sender_ack_after_ms": 21.856674
|
||||
},
|
||||
{
|
||||
"delay_ms_per_sync": 100,
|
||||
"phase": "settled",
|
||||
"samples_per_binary": 3,
|
||||
"body_before_ms": 1232.468024,
|
||||
"body_after_ms": 419.683064,
|
||||
"sender_ack_before_ms": 1641.598264,
|
||||
"sender_ack_after_ms": 644.082891
|
||||
},
|
||||
{
|
||||
"delay_ms_per_sync": 100,
|
||||
"phase": "avatar_pending",
|
||||
"samples_per_binary": 3,
|
||||
"body_before_ms": 2683.292251,
|
||||
"body_after_ms": 1378.231158,
|
||||
"sender_ack_before_ms": 3195.209226,
|
||||
"sender_ack_after_ms": 1581.975429
|
||||
},
|
||||
{
|
||||
"delay_ms_per_sync": 200,
|
||||
"phase": "settled",
|
||||
"samples_per_binary": 1,
|
||||
"body_before_ms": 2836.63993,
|
||||
"body_after_ms": 2042.56345,
|
||||
"sender_ack_before_ms": 3643.297905,
|
||||
"sender_ack_after_ms": 2441.985416
|
||||
},
|
||||
{
|
||||
"delay_ms_per_sync": 200,
|
||||
"phase": "avatar_pending",
|
||||
"samples_per_binary": 1,
|
||||
"body_before_ms": 5782.018326,
|
||||
"body_after_ms": 2676.283903,
|
||||
"sender_ack_before_ms": 6595.924675,
|
||||
"sender_ack_after_ms": 3094.949323
|
||||
}
|
||||
],
|
||||
"durability_crash": {
|
||||
"status": "pass",
|
||||
"delay_ms_per_sync": 100,
|
||||
"acknowledged_messages": 3,
|
||||
"mam_records_per_account": [
|
||||
3,
|
||||
3
|
||||
],
|
||||
"resumed_resources": 4,
|
||||
"replayed_personal_deliveries": 9,
|
||||
"offline_checkpoint": {
|
||||
"exported_sessions": 4,
|
||||
"native_archives": 2,
|
||||
"journals_removed": true,
|
||||
"second_invocation_idempotent": true
|
||||
},
|
||||
"pending_before_reauthentication": [
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
3
|
||||
],
|
||||
"sync_between_publication_and_sigkill": {
|
||||
"total": 32,
|
||||
"by_store_kind_op": {
|
||||
"mam:directory:fsync": 2,
|
||||
"mam:file:fsync": 10,
|
||||
"pep:directory:fsync": 2,
|
||||
"pep:file:fsync": 2,
|
||||
"sm:file:fdatasync": 16
|
||||
}
|
||||
},
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
|
||||
"scope": "loopback STARTTLS; real delayed fsync/fdatasync; sender SM acknowledgement then SIGKILL; authoritative queues and MAM checked before reauthentication; at-least-once replay"
|
||||
},
|
||||
"recovery_sync_faults": {
|
||||
"status": "pass",
|
||||
"cases": [
|
||||
{
|
||||
"suffix": ".wal",
|
||||
"status": "pass",
|
||||
"checkpoint_rejected_sync_failure": true,
|
||||
"journals_preserved_on_failure": 4,
|
||||
"serve_rejected_before_listeners": true,
|
||||
"clean_retry_preserved_sessions": 4,
|
||||
"clean_retry_preserved_archives": 2
|
||||
},
|
||||
{
|
||||
"suffix": ".rredo",
|
||||
"status": "pass",
|
||||
"checkpoint_rejected_sync_failure": true,
|
||||
"journals_preserved_on_failure": 2,
|
||||
"serve_rejected_before_listeners": false,
|
||||
"clean_retry_preserved_sessions": 4,
|
||||
"clean_retry_preserved_archives": 2
|
||||
}
|
||||
],
|
||||
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a"
|
||||
},
|
||||
"limitations": [
|
||||
"MinIO upstream RELEASE.2025-04-22T22-12-26Z with disclosed five-line EPERM loopback interface enumeration adapter; S3/auth/storage/multipart code unchanged",
|
||||
"Single-node loopback MinIO, HTTP gateway-to-store and verified HTTPS client-to-gateway; no AWS account or distributed/production storage qualification",
|
||||
"Missing/corrupt ownership metadata cannot identify arbitrary bucket orphans; lifecycle remains defense in depth",
|
||||
"Ambiguous completion with HEAD404 quarantines slot until a trustworthy remote outcome; no automatic destructive reset",
|
||||
"Migration is offline, source preserving, requires temporary disk up to one largest file; no live migration",
|
||||
"Previous full Prosody parity / production client / DNSSEC limits unchanged"
|
||||
"Loopback fixture, not production client or disk qualification",
|
||||
"0ms pair ran sequentially without full verify; 100/200ms final runs had background final verify",
|
||||
"One200ms sample; startup/migration/rotation and background heartbeat affect sync counts",
|
||||
"SM global IO mutex and full snapshots remain; no atomic transaction spanning MAM/SM/network",
|
||||
"Offline storage-checkpoint required before legacy/native writer handoff",
|
||||
"10GiB transfers, real MinIO/AWS, Docker/Gitea/GitHub run were not repeated in this release"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate Gitea publication inputs and emit literal, shell-safe image tags."""
|
||||
import os
|
||||
import re
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def image_tags(env, version):
|
||||
registry = env.get('GITEA_REGISTRY', '')
|
||||
image = env.get('GITEA_IMAGE', '')
|
||||
if not re.fullmatch(r'[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::[0-9]{1,5})?', registry):
|
||||
raise ValueError('GITEA_REGISTRY must be a lowercase hostname[:port], without scheme or path')
|
||||
if not re.fullmatch(r'[a-z0-9]+(?:[._-][a-z0-9]+)*(?:/[a-z0-9]+(?:[._-][a-z0-9]+)*)+', image):
|
||||
raise ValueError('GITEA_IMAGE must be lowercase owner/image, without registry or tag')
|
||||
if not env.get('REGISTRY_USER') or not env.get('REGISTRY_TOKEN'):
|
||||
raise ValueError('Set GITEA_REGISTRY_USERNAME and GITEA_REGISTRY_TOKEN secrets')
|
||||
sha = env['GITHUB_SHA']
|
||||
if not re.fullmatch(r'[0-9a-f]{40}', sha):
|
||||
raise ValueError('Invalid commit SHA')
|
||||
base = f'{registry}/{image}'
|
||||
tags = [f'{base}:sha-{sha}']
|
||||
ref = env['GITHUB_REF']
|
||||
if ref.startswith('refs/tags/'):
|
||||
release = ref.removeprefix('refs/tags/')
|
||||
if release != f'v{version}' or not re.fullmatch(r'v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?', release):
|
||||
raise ValueError('Release tag must be v<Cargo.toml package.version> (no build metadata)')
|
||||
tags.append(f'{base}:{release.lower()}')
|
||||
elif ref == 'refs/heads/' + env['DEFAULT_BRANCH']:
|
||||
tags.append(f'{base}:latest')
|
||||
else:
|
||||
raise ValueError('Only default branch or version tags may publish')
|
||||
return tags
|
||||
|
||||
|
||||
def main():
|
||||
version = tomllib.loads(Path('Cargo.toml').read_text())['package']['version']
|
||||
tags = image_tags(os.environ, version)
|
||||
with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as out:
|
||||
out.write('tags<<IMAGE_TAGS\n' + '\n'.join(tags) + '\nIMAGE_TAGS\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Unmodified upstream source; never apply the sandbox adapter in hosted CI.
|
||||
set -euo pipefail
|
||||
output=${1:?Usage: prepare-minio.sh ABSOLUTE_OUTPUT_BINARY}
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
curl --fail --location --retry 3 --connect-timeout 20 --max-time 300 \
|
||||
https://github.com/minio/minio/archive/refs/tags/RELEASE.2025-04-22T22-12-26Z.tar.gz \
|
||||
--output "$work/minio.tar.gz"
|
||||
printf '%s %s\n' 1a63209927b9d1fb239b2a776b7b01d1902eff19379a108f4bc7005edcc0968c "$work/minio.tar.gz" | sha256sum --check --strict
|
||||
tar --no-same-owner -xzf "$work/minio.tar.gz" -C "$work"
|
||||
cd "$work/minio-RELEASE.2025-04-22T22-12-26Z"
|
||||
mkdir -p "$(dirname "$output")"
|
||||
GOTOOLCHAIN=local go build -mod=readonly -p 2 -o "$output" .
|
||||
"$output" --version
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
reference_parent=${1:?Usage: prepare-reference.sh OUTPUT_DIRECTORY}
|
||||
mkdir -p "$reference_parent"
|
||||
reference_parent=$(cd -- "$reference_parent" && pwd)
|
||||
cd "$project_dir/scripts/ci/vendor"
|
||||
printf '%s\n' '1e558d3d2211ea0228596aced2b71306503190c454ec4cb44f172045b3513c34 prosody-cafd74b9c5ea.tar.gz' | sha256sum --check --strict
|
||||
tar --no-same-owner -xzf prosody-cafd74b9c5ea.tar.gz -C "$reference_parent"
|
||||
cd "$reference_parent/prosody-cafd74b9c5ea"
|
||||
./configure --ostype=debian --lua-version=5.4 --lua-suffix=5.4
|
||||
make -j2
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
# Native reference test source
|
||||
|
||||
`prosody-cafd74b9c5ea.tar.gz` is the original, unmodified source archive supplied
|
||||
with this project. Revision: `cafd74b9c5ead46eb58910359eb266e1732b0aa1`.
|
||||
SHA-256: `1e558d3d2211ea0228596aced2b71306503190c454ec4cb44f172045b3513c34`.
|
||||
|
||||
The source is bundled so CI cannot silently skip native oracle tests when a
|
||||
private upload URL expires. Prosody's MIT/X11 license and copyright notices
|
||||
are retained inside the archive (`COPYING`). No compiled shared objects are
|
||||
bundled. `prepare-reference.sh` checks the archive hash and builds native
|
||||
modules against the runner's Lua/OpenSSL/ICU installation.
|
||||
BIN
Binary file not shown.
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
: "${PROSODY_REFERENCE_SOURCE:?Build the bundled reference first}"
|
||||
: "${MINIO_BINARY:?Build genuine MinIO first}"
|
||||
export XMPP_LOAD_CHECK=1
|
||||
mkdir -p ci-results
|
||||
bash scripts/verify-local.sh
|
||||
# These two tests are deliberately ignored by the ordinary test command.
|
||||
cargo test --locked --test admin_socket --test archive_mam -- --ignored --nocapture
|
||||
python3 tests/upload_large_network.py --binary target/release/rust-xmpp --report ci-results/upload-local-10gib.json
|
||||
@@ -18,6 +18,7 @@ python3 tests/wss_interop.py --binary target/release/rust-xmpp
|
||||
python3 tests/http_proxy_paths_network.py --binary target/release/rust-xmpp
|
||||
python3 tests/http_altconnect_network.py --binary target/release/rust-xmpp
|
||||
python3 tests/sasl2_bind.py target/release/rust-xmpp
|
||||
python3 tests/bind2_archive_latency_network.py --binary target/release/rust-xmpp --records 10000 --samples 2 --output /tmp/bind2-archive-latency.json
|
||||
python3 tests/sasl2_cached_inline.py target/release/rust-xmpp
|
||||
python3 tests/gajim_discovery_network.py target/release/rust-xmpp
|
||||
python3 tests/sasl2_resume.py target/release/rust-xmpp
|
||||
@@ -27,6 +28,16 @@ python3 tests/pep_order_network.py target/release/rust-xmpp
|
||||
python3 tests/pep_notifications_network.py target/release/rust-xmpp
|
||||
python3 tests/preauth_network.py target/release/rust-xmpp
|
||||
python3 tests/muc_restart_network.py target/release/rust-xmpp
|
||||
python3 tests/muc_crash_network.py target/release/rust-xmpp
|
||||
python3 tests/sm_restart_network.py target/release/rust-xmpp
|
||||
python3 tests/slow_storage_latency_network.py --binary target/release/rust-xmpp --delay-ms 0 25 --rounds 1 --output /tmp/slow-storage-latency.json
|
||||
python3 tests/slow_storage_crash_network.py --binary target/release/rust-xmpp --delay-ms 25 --messages 3 --output /tmp/slow-storage-crash.json
|
||||
python3 tests/journal_recovery_sync_network.py --binary target/release/rust-xmpp --output /tmp/journal-recovery-sync.json
|
||||
python3 tests/mam_broken_baseline_network.py --binary target/release/rust-xmpp
|
||||
python3 tests/push_latency_network.py --binary target/release/rust-xmpp --recipient hibernating --output /tmp/push-latency.json
|
||||
python3 tests/push_latency_network.py --binary target/release/rust-xmpp --recipient offline --output /tmp/push-offline-latency.json
|
||||
python3 tests/personal_message_latency_network.py --binary target/release/rust-xmpp --rounds 1 --history-records 1000 --roster-sizes 0 --backlog 30 --csi-dwell 0.1 --output /tmp/personal-message-latency.json
|
||||
python3 tests/message_latency_network.py --binary target/release/rust-xmpp --rounds 3 --history-records 0 10000 --output /tmp/xmpp-message-latency.json
|
||||
python3 tests/resource_rebind.py target/release/rust-xmpp
|
||||
python3 scripts/integration_http.py
|
||||
python3 scripts/integration_http.py --custom-upload-path
|
||||
@@ -39,6 +50,7 @@ if [[ -n ${MINIO_BINARY:-} ]]; then
|
||||
fi
|
||||
if [[ ${XMPP_LOAD_CHECK:-0} == 1 ]]; then
|
||||
python3 scripts/load_smoke.py --clients 500 --accounts 100 --rounds 10 --message-rounds 10 --archive
|
||||
python3 tests/muc_sm_load_network.py --binary target/release/rust-xmpp --clients 500
|
||||
fi
|
||||
if [[ -n ${PROSODY_REFERENCE_SOURCE:-} ]]; then
|
||||
RUSTUP_TOOLCHAIN=${RUSTUP_TOOLCHAIN:-1.99.1} scripts/prosody_compat/verify.sh \
|
||||
|
||||
+742
-51
@@ -5,7 +5,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Read, Write},
|
||||
io::{BufRead, BufReader, Read, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
sync::{Arc, Mutex, OnceLock, Weak},
|
||||
};
|
||||
@@ -174,7 +174,103 @@ pub fn scan_store<F: FnMut(Entry) -> Result<()>>(
|
||||
let _guard = lock.lock().map_err(|_| anyhow!("archive lock poisoned"))?;
|
||||
scan_unlocked(root, host, user, store, visit)
|
||||
}
|
||||
fn recover_append(p: &Path) -> Result<()> {
|
||||
// The native file remains directly readable by Prosody. While this sidecar exists,
|
||||
// its complete frames define the committed suffix: do not edit a live native file
|
||||
// in place or replace it until checkpoint_all has durably retired the journal.
|
||||
const REDO_MAGIC: &[u8; 8] = b"XARRED01";
|
||||
const REDO_HEADER: usize = 96;
|
||||
const REDO_LIMIT: u64 = 1024 * 1024;
|
||||
const REDO_FRAMES: usize = 128;
|
||||
#[derive(Clone)]
|
||||
struct RedoState {
|
||||
length: u64,
|
||||
bytes: u64,
|
||||
frames: usize,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct FileStamp {
|
||||
len: u64,
|
||||
modified: Option<std::time::SystemTime>,
|
||||
identity: (u64, u64),
|
||||
#[cfg(unix)]
|
||||
changed: (i64, i64),
|
||||
}
|
||||
fn identity(meta: &std::fs::Metadata) -> (u64, u64) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
(meta.dev(), meta.ino())
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = meta;
|
||||
(0, 0)
|
||||
}
|
||||
}
|
||||
fn stamp(p: &Path) -> Result<FileStamp> {
|
||||
let m = std::fs::metadata(p)?;
|
||||
Ok(FileStamp {
|
||||
len: m.len(),
|
||||
modified: m.modified().ok(),
|
||||
identity: identity(&m),
|
||||
#[cfg(unix)]
|
||||
changed: {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
(m.ctime(), m.ctime_nsec())
|
||||
},
|
||||
})
|
||||
}
|
||||
type RedoCache = std::collections::HashMap<std::path::PathBuf, (FileStamp, FileStamp, RedoState)>;
|
||||
static REDO_CACHE: OnceLock<Mutex<RedoCache>> = OnceLock::new();
|
||||
fn remember_redo(p: &Path, state: &RedoState) -> Result<()> {
|
||||
let native = stamp(p)?;
|
||||
let journal = stamp(&p.with_extension("rredo"))?;
|
||||
let mut cache = REDO_CACHE
|
||||
.get_or_init(|| Mutex::new(RedoCache::new()))
|
||||
.lock()
|
||||
.map_err(|_| anyhow!("archive redo cache poisoned"))?;
|
||||
if cache.len() >= 1024 {
|
||||
cache.clear();
|
||||
}
|
||||
cache.insert(p.to_path_buf(), (native, journal, state.clone()));
|
||||
Ok(())
|
||||
}
|
||||
fn digest(data: &[u8]) -> [u8; 32] {
|
||||
use sha2::{Digest, Sha256};
|
||||
Sha256::digest(data).into()
|
||||
}
|
||||
fn baseline_anchor(file: &mut std::fs::File, length: u64) -> Result<[u8; 32]> {
|
||||
let count = length.min(4096);
|
||||
file.seek(SeekFrom::Start(length - count))?;
|
||||
let mut bytes = vec![0; count as usize];
|
||||
file.read_exact(&mut bytes)?;
|
||||
Ok(digest(&bytes))
|
||||
}
|
||||
/// Native fsync precedes journal replacement. Either old or new header can
|
||||
/// survive a crash, and both refer to an already durable native baseline.
|
||||
fn checkpoint_redo(p: &Path) -> Result<RedoState> {
|
||||
let mut file = std::fs::OpenOptions::new().read(true).write(true).open(p)?;
|
||||
file.sync_all()?;
|
||||
let meta = file.metadata()?;
|
||||
let length = meta.len();
|
||||
let (dev, ino) = identity(&meta);
|
||||
let mut header = Vec::with_capacity(REDO_HEADER);
|
||||
header.extend_from_slice(REDO_MAGIC);
|
||||
for n in [length, dev, ino] {
|
||||
header.extend_from_slice(&n.to_le_bytes());
|
||||
}
|
||||
header.extend_from_slice(&baseline_anchor(&mut file, length)?);
|
||||
header.extend_from_slice(&digest(&header));
|
||||
storage::atomic_write(&p.with_extension("rredo"), &header)?;
|
||||
let state = RedoState {
|
||||
length,
|
||||
bytes: REDO_HEADER as u64,
|
||||
frames: 0,
|
||||
};
|
||||
remember_redo(p, &state)?;
|
||||
Ok(state)
|
||||
}
|
||||
fn recover_legacy_append(p: &Path) -> Result<()> {
|
||||
let journal = p.with_extension("rappend");
|
||||
match std::fs::read(&journal) {
|
||||
Ok(data) => {
|
||||
@@ -195,6 +291,263 @@ fn recover_append(p: &Path) -> Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn recover_redo(p: &Path) -> Result<Option<RedoState>> {
|
||||
recover_legacy_append(p)?;
|
||||
let journal = p.with_extension("rredo");
|
||||
let jm = match stamp(&journal) {
|
||||
Ok(m) => m,
|
||||
Err(e)
|
||||
if e.downcast_ref::<std::io::Error>()
|
||||
.is_some_and(|e| e.kind() == std::io::ErrorKind::NotFound) =>
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let nm = stamp(p).ok();
|
||||
if let Some(native) = &nm {
|
||||
let cache = REDO_CACHE
|
||||
.get_or_init(|| Mutex::new(RedoCache::new()))
|
||||
.lock()
|
||||
.map_err(|_| anyhow!("archive redo cache poisoned"))?;
|
||||
if let Some((n, j, state)) = cache.get(p)
|
||||
&& n == native
|
||||
&& j == &jm
|
||||
{
|
||||
return Ok(Some(state.clone()));
|
||||
}
|
||||
}
|
||||
// Defer the recovery body so an untrusted-baseline failure is handled once:
|
||||
// the journal is retired and the durable synced native becomes authoritative.
|
||||
let recovered = (|| -> Result<Option<RedoState>> {
|
||||
// One oversized record is allowed, but the journal never grows without bound.
|
||||
anyhow::ensure!(
|
||||
jm.len <= REDO_LIMIT + MAX_RECORD_BYTES as u64 + REDO_HEADER as u64 + 72,
|
||||
"archive redo journal exceeds bound"
|
||||
);
|
||||
// Read only the fixed header first. Validating the baseline anchor against
|
||||
// the native file requires only the header; the expensive part (reading and
|
||||
// checksumming every frame) is deferred until we know the journal is a
|
||||
// trusted match. This keeps a large `.rredo` with a broken baseline from
|
||||
// costing seconds on every access, which is exactly the work Prosody never
|
||||
// performs.
|
||||
let mut header = [0u8; REDO_HEADER];
|
||||
let mut jf = std::fs::File::open(&journal)?;
|
||||
let header_len = jf.read(&mut header)?;
|
||||
anyhow::ensure!(
|
||||
header_len == REDO_HEADER && &header[..8] == REDO_MAGIC,
|
||||
"invalid archive redo header"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
digest(&header[..64]) == header[64..96],
|
||||
"archive redo header checksum mismatch"
|
||||
);
|
||||
let base = u64::from_le_bytes(header[8..16].try_into().unwrap());
|
||||
// Inodes change when an archive is copied/restored. The journal remains
|
||||
// authoritative across that change; identity is only a cache stamp.
|
||||
anyhow::ensure!(nm.is_some() || base == 0, "missing archive redo baseline");
|
||||
{
|
||||
let mut opts = std::fs::OpenOptions::new();
|
||||
opts.read(true).write(true).create(true).truncate(false);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
opts.mode(0o600);
|
||||
}
|
||||
let mut native = opts.open(p)?;
|
||||
anyhow::ensure!(
|
||||
native.metadata()?.len() >= base,
|
||||
"archive shorter than redo baseline"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
baseline_anchor(&mut native, base)? == header[32..64],
|
||||
"archive redo baseline changed in place"
|
||||
);
|
||||
}
|
||||
// Trusted baseline. Now read and validate the remainder of the journal and
|
||||
// replay its frames into the native file (durable redo recovery).
|
||||
let data = std::fs::read(&journal)?;
|
||||
let mut pos = REDO_HEADER;
|
||||
let mut frames = Vec::new();
|
||||
let mut length = base;
|
||||
while pos + 72 <= data.len() {
|
||||
anyhow::ensure!(
|
||||
digest(&data[pos..pos + 8]) == data[pos + 8..pos + 40],
|
||||
"archive redo frame length checksum mismatch"
|
||||
);
|
||||
let size = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
|
||||
anyhow::ensure!(
|
||||
size > 0 && size <= MAX_RECORD_BYTES as u64,
|
||||
"invalid archive redo frame length"
|
||||
);
|
||||
let end = pos + 72 + size as usize;
|
||||
if end > data.len() {
|
||||
break;
|
||||
}
|
||||
let payload = &data[pos + 72..end];
|
||||
anyhow::ensure!(
|
||||
digest(payload) == data[pos + 40..pos + 72],
|
||||
"archive redo frame checksum mismatch"
|
||||
);
|
||||
frames.push((length, payload));
|
||||
anyhow::ensure!(
|
||||
frames.len() <= REDO_FRAMES,
|
||||
"archive redo frame count exceeds bound"
|
||||
);
|
||||
length = length
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| anyhow!("archive redo offset overflow"))?;
|
||||
pos = end;
|
||||
}
|
||||
drop(jf);
|
||||
// Native indexes are rebuildable. A crash may have resurrected an unlink
|
||||
// from the append path; invalidate it again before exposing recovered data.
|
||||
match std::fs::remove_file(p.with_extension("lidx")) {
|
||||
Ok(()) => std::fs::File::open(p.parent().unwrap())?.sync_all()?,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
// A complete frame can survive a process crash in page cache even if its
|
||||
// writer never completed sync. Durably adopt all validated frames before
|
||||
// exposing them or allowing later recovery to truncate the native file.
|
||||
let redo = std::fs::OpenOptions::new().write(true).open(&journal)?;
|
||||
if pos < data.len() {
|
||||
redo.set_len(pos as u64)?;
|
||||
}
|
||||
redo.sync_all()?;
|
||||
// Validate the complete journal before changing native data. Replaying is
|
||||
// idempotent, including after a second crash midway through recovery.
|
||||
let mut file = std::fs::OpenOptions::new().read(true).write(true).open(p)?;
|
||||
let mut changed = file.metadata()?.len() != length;
|
||||
for (offset, payload) in &frames {
|
||||
file.seek(SeekFrom::Start(*offset))?;
|
||||
let mut native = vec![0; payload.len()];
|
||||
let equal = file.read_exact(&mut native).is_ok() && native.as_slice() == *payload;
|
||||
if !equal {
|
||||
file.seek(SeekFrom::Start(*offset))?;
|
||||
file.write_all(payload)?;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
file.set_len(length)?;
|
||||
file.sync_all()?;
|
||||
}
|
||||
let state = RedoState {
|
||||
length,
|
||||
bytes: pos as u64,
|
||||
frames: frames.len(),
|
||||
};
|
||||
if nm.is_none() {
|
||||
return checkpoint_redo(p).map(Some);
|
||||
}
|
||||
remember_redo(p, &state)?;
|
||||
Ok(Some(state))
|
||||
})();
|
||||
match recovered {
|
||||
Err(error) if is_untrusted_redo(&error) => {
|
||||
// The journal no longer matches its native baseline. Prosody itself
|
||||
// would keep appending to the native file without any redo view:
|
||||
// adopt that currently synced native as authoritative, retire the
|
||||
// untrusted journal, and report "no redo". This keeps archive access
|
||||
// at Prosody-level speed and makes the archive writable again without
|
||||
// an explicit offline handoff, matching how the same data behaves
|
||||
// under Prosody.
|
||||
retire_redo(p)?;
|
||||
Ok(None)
|
||||
}
|
||||
Ok(state) => Ok(state),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
/// Recovery for read-only archive access. A journal whose baseline an external
|
||||
/// writer invalidated cannot be replayed safely; treat the durably synced native
|
||||
/// as authoritative and read it as-is instead of failing every edge/scan/query.
|
||||
/// Returns `Ok(true)` only when the baseline is untrusted (caller should proceed
|
||||
/// on native), `Ok(false)` after a normal recovery, or `Err` for other failures.
|
||||
fn recover_append_readonly(p: &Path) -> Result<bool> {
|
||||
match recover_redo(p) {
|
||||
Ok(_) => Ok(false),
|
||||
Err(error) if is_untrusted_redo(&error) => Ok(true),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
/// Durable handoff to a native writer. Call after stopping archive users and
|
||||
/// before letting Prosody modify `.list` files in place. Abrupt shutdown leaves
|
||||
/// redo recovery active; unjournaled native suffixes are then uncommitted.
|
||||
pub fn checkpoint_all(root: &Path) -> Result<usize> {
|
||||
with_exclusive_access(|| {
|
||||
let mut pending = vec![root.to_path_buf()];
|
||||
let mut count = 0;
|
||||
while let Some(dir) = pending.pop() {
|
||||
let entries = match std::fs::read_dir(&dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let kind = entry.file_type()?;
|
||||
if kind.is_dir() {
|
||||
pending.push(entry.path());
|
||||
} else if kind.is_file() && entry.path().extension().is_some_and(|e| e == "rredo") {
|
||||
let path = entry.path().with_extension("list");
|
||||
prepare_rewrite(&path)?;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
})
|
||||
}
|
||||
/// Retire redo before an atomic snapshot replacement. The old native snapshot
|
||||
/// is synced first, so a crash after unlink and before rename retains every
|
||||
/// committed record. After rename there is no old redo to resurrect deletions.
|
||||
fn retire_redo(p: &Path) -> Result<()> {
|
||||
// The native file may not exist when the journal arrived without one (e.g.
|
||||
// a `missing baseline` journal); it is still fine to retire the sidecar.
|
||||
match std::fs::File::open(p) {
|
||||
Ok(f) => f.sync_all()?,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
std::fs::remove_file(p.with_extension("rredo"))?;
|
||||
std::fs::File::open(p.parent().unwrap())?.sync_all()?;
|
||||
REDO_CACHE
|
||||
.get_or_init(|| Mutex::new(RedoCache::new()))
|
||||
.lock()
|
||||
.map_err(|_| anyhow!("archive redo cache poisoned"))?
|
||||
.remove(p);
|
||||
Ok(())
|
||||
}
|
||||
/// True when the redo journal no longer matches its native baseline after an
|
||||
/// external in-place write or replacement. In that state the journal is no
|
||||
/// longer authoritative and must not be replayed over the native file.
|
||||
fn is_untrusted_redo(error: &anyhow::Error) -> bool {
|
||||
let text = format!("{error:#}");
|
||||
text.contains("archive redo baseline changed in place")
|
||||
|| text.contains("archive shorter than redo baseline")
|
||||
|| text.contains("missing archive redo baseline")
|
||||
|| text.contains("archive redo journal exceeds bound")
|
||||
}
|
||||
/// Retire redo before an atomic snapshot replacement or with an explicit offline
|
||||
/// handoff (storage-checkpoint). A journal whose baseline was externally
|
||||
/// rewritten in place can no longer be trusted: because the caller is about to
|
||||
/// replace the native file (rewrite) or to hand control of it to a native
|
||||
/// writer (checkpoint), the current durably synced native becomes authoritative
|
||||
/// and the untrusted journal is retired rather than failing the operation.
|
||||
fn prepare_rewrite(p: &Path) -> Result<()> {
|
||||
match recover_redo(p) {
|
||||
Ok(Some(_)) => retire_redo(p),
|
||||
Ok(None) => Ok(()),
|
||||
Err(error) if is_untrusted_redo(&error) => {
|
||||
// The journal no longer matches its baseline. Treat the native file
|
||||
// as authoritative, sync it, and retire the untrusted journal.
|
||||
retire_redo(p)
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
fn scan_unlocked<F: FnMut(Entry) -> Result<()>>(
|
||||
root: &Path,
|
||||
host: &str,
|
||||
@@ -203,7 +556,7 @@ fn scan_unlocked<F: FnMut(Entry) -> Result<()>>(
|
||||
mut visit: F,
|
||||
) -> Result<()> {
|
||||
let path = storage::path(root, host, user, store).with_extension("list");
|
||||
recover_append(&path)?;
|
||||
recover_append_readonly(&path)?;
|
||||
let file = match std::fs::File::open(&path) {
|
||||
Ok(f) => f,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
||||
@@ -212,46 +565,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 +590,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_readonly(&path)?;
|
||||
let mut file = match std::fs::File::open(&path) {
|
||||
Ok(file) => file,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let decoder = RecordDecoder::new()?;
|
||||
let first = match first_record(&mut BufReader::new(&mut file), &decoder)? {
|
||||
Some(entry) => entry,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let length = file.metadata()?.len();
|
||||
let mut window = 8192u64.min(length);
|
||||
loop {
|
||||
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| {
|
||||
@@ -345,7 +811,7 @@ fn save_unlocked(
|
||||
data.push_str(&encode_entry(e)?);
|
||||
}
|
||||
let p = storage::path(root, host, user, store).with_extension("list");
|
||||
recover_append(&p)?;
|
||||
prepare_rewrite(&p)?;
|
||||
match std::fs::remove_file(p.with_extension("lidx")) {
|
||||
Ok(()) => (),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
|
||||
@@ -406,37 +872,57 @@ fn append_unlocked(
|
||||
entry: &Entry,
|
||||
) -> Result<()> {
|
||||
let data = encode_entry(entry)?;
|
||||
anyhow::ensure!(
|
||||
data.len() <= MAX_RECORD_BYTES,
|
||||
"archive record exceeds 16 MiB"
|
||||
);
|
||||
let path = storage::path(root, host, user, store).with_extension("list");
|
||||
let parent = path.parent().unwrap();
|
||||
std::fs::create_dir_all(parent)?;
|
||||
recover_append(&path)?;
|
||||
std::fs::create_dir_all(path.parent().unwrap())?;
|
||||
let state = recover_redo(&path)?;
|
||||
let mut opts = std::fs::OpenOptions::new();
|
||||
opts.create(true).append(true);
|
||||
opts.create(true).read(true).write(true).truncate(false);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
opts.mode(0o600);
|
||||
}
|
||||
let mut file = opts.open(&path)?;
|
||||
let length = file.metadata()?.len();
|
||||
let journal = path.with_extension("rappend");
|
||||
storage::atomic_write(&journal, &length.to_le_bytes())?;
|
||||
let mut state = match state {
|
||||
Some(state) => state,
|
||||
None => checkpoint_redo(&path)?,
|
||||
};
|
||||
if state.bytes >= REDO_LIMIT || state.frames >= REDO_FRAMES {
|
||||
state = checkpoint_redo(&path)?;
|
||||
}
|
||||
match std::fs::remove_file(path.with_extension("lidx")) {
|
||||
Ok(()) => (),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
let result = (|| -> Result<()> {
|
||||
file.write_all(data.as_bytes())?;
|
||||
file.sync_all()?;
|
||||
std::fs::remove_file(&journal)?;
|
||||
std::fs::File::open(parent)?.sync_all()?;
|
||||
Ok(())
|
||||
})();
|
||||
if result.is_err() {
|
||||
recover_append(&path)?;
|
||||
}
|
||||
result
|
||||
let mut frame = Vec::with_capacity(72 + data.len());
|
||||
frame.extend_from_slice(&(data.len() as u64).to_le_bytes());
|
||||
frame.extend_from_slice(&digest(&(data.len() as u64).to_le_bytes()));
|
||||
frame.extend_from_slice(&digest(data.as_bytes()));
|
||||
frame.extend_from_slice(data.as_bytes());
|
||||
// Invalidate the cache before any write, including error paths. A failed
|
||||
// sync is indeterminate and must be recovered, never blindly rolled back.
|
||||
REDO_CACHE
|
||||
.get_or_init(|| Mutex::new(RedoCache::new()))
|
||||
.lock()
|
||||
.map_err(|_| anyhow!("archive redo cache poisoned"))?
|
||||
.remove(&path);
|
||||
file.seek(SeekFrom::Start(state.length))?;
|
||||
file.write_all(data.as_bytes())?;
|
||||
let mut journal = std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(path.with_extension("rredo"))?;
|
||||
journal.write_all(&frame)?;
|
||||
journal.sync_all()?;
|
||||
state.length += data.len() as u64;
|
||||
state.bytes += frame.len() as u64;
|
||||
state.frames += 1;
|
||||
remember_redo(&path, &state)?;
|
||||
Ok(())
|
||||
}
|
||||
/// Owner is localpart, peer is bare JID. Caller enforces archive preferences.
|
||||
pub fn archive_message(
|
||||
@@ -501,6 +987,7 @@ fn retain_unlocked<F: FnMut(&Entry) -> bool>(
|
||||
mut retain: F,
|
||||
) -> Result<usize> {
|
||||
let path = storage::path(root, host, user, store).with_extension("list");
|
||||
recover_append_readonly(&path)?;
|
||||
if !path.exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
@@ -528,6 +1015,7 @@ fn retain_unlocked<F: FnMut(&Entry) -> bool>(
|
||||
return Ok(0);
|
||||
}
|
||||
file.sync_all()?;
|
||||
prepare_rewrite(&path)?;
|
||||
match std::fs::remove_file(path.with_extension("lidx")) {
|
||||
Ok(()) => (),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
|
||||
@@ -540,3 +1028,206 @@ fn retain_unlocked<F: FnMut(&Entry) -> bool>(
|
||||
let _ = std::fs::remove_file(&temp);
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod redo_rewrite_tests {
|
||||
use super::*;
|
||||
|
||||
fn fixture() -> (tempfile::TempDir, std::path::PathBuf) {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
append_new(
|
||||
root.path(),
|
||||
"example.org",
|
||||
Some("alice"),
|
||||
"archive",
|
||||
Entry {
|
||||
id: "committed-before-rewrite".into(),
|
||||
when: 42.,
|
||||
peer: String::new(),
|
||||
xml: "<message><body>durable</body></message>".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let path = storage::path(root.path(), "example.org", Some("alice"), "archive")
|
||||
.with_extension("list");
|
||||
(root, path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crash_gap_after_redo_retirement_before_native_rename_keeps_old_snapshot() {
|
||||
let (root, path) = fixture();
|
||||
let expected = std::fs::read(&path).unwrap();
|
||||
prepare_rewrite(&path).unwrap();
|
||||
// Crash boundary: redo unlink and directory sync completed, replacement
|
||||
// has not happened. Restart must retain the complete durable old list.
|
||||
assert!(!path.with_extension("rredo").exists());
|
||||
assert_eq!(std::fs::read(&path).unwrap(), expected);
|
||||
let rows = load(root.path(), "example.org", "alice").unwrap();
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0].id, "committed-before-rewrite");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crash_gap_after_native_rename_has_no_old_redo_to_resurrect_deletions() {
|
||||
let (root, path) = fixture();
|
||||
prepare_rewrite(&path).unwrap();
|
||||
storage::atomic_write(&path, b"").unwrap();
|
||||
// Crash boundary: the atomic replacement committed. Recovery sees the
|
||||
// new snapshot directly and cannot replay the removed old record.
|
||||
assert!(!path.with_extension("rredo").exists());
|
||||
assert!(
|
||||
load(root.path(), "example.org", "alice")
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
/// Build an archive large enough to have rolled its redo journal, so the
|
||||
/// baseline is non-empty (`base > 0`) and the anchor guard is meaningful.
|
||||
fn rolled_fixture() -> (tempfile::TempDir, std::path::PathBuf) {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
// One more than REDO_FRAMES forces a redo rotation at frame 128, which
|
||||
// snapshots the current native length as the new non-empty baseline.
|
||||
for i in 0..=REDO_FRAMES as u64 {
|
||||
append_new(
|
||||
root.path(),
|
||||
"example.org",
|
||||
Some("alice"),
|
||||
"archive",
|
||||
Entry {
|
||||
id: format!("roll-{i}"),
|
||||
when: i as f64,
|
||||
peer: String::new(),
|
||||
xml: "<message><body>x</body></message>".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let path = storage::path(root.path(), "example.org", Some("alice"), "archive")
|
||||
.with_extension("list");
|
||||
// Recovery must see a non-empty baseline (rotation happened).
|
||||
let data = std::fs::read(path.with_extension("rredo")).unwrap();
|
||||
let base = u64::from_le_bytes(data[8..16].try_into().unwrap());
|
||||
assert!(base > 0, "fixture should have rolled a non-empty baseline");
|
||||
(root, path)
|
||||
}
|
||||
|
||||
/// An external writer that rewrote the native archive in place over the
|
||||
/// committed baseline without a handoff leaves the redo journal untrusted.
|
||||
fn corrupt_baseline(path: &std::path::Path) -> u64 {
|
||||
let data = std::fs::read(path.with_extension("rredo")).unwrap();
|
||||
let base = u64::from_le_bytes(data[8..16].try_into().unwrap());
|
||||
let mut native = std::fs::read(path).unwrap();
|
||||
// Flip the trailing bytes of the committed baseline so its anchor changes.
|
||||
let start = (base.saturating_sub(64)) as usize;
|
||||
for i in start..native.len().min(base as usize) {
|
||||
native[i] ^= 0x5a;
|
||||
}
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(false)
|
||||
.open(path)
|
||||
.unwrap();
|
||||
std::io::Write::write_all(&mut file, &native).unwrap();
|
||||
file.sync_all().unwrap();
|
||||
base
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_append_recovers_by_retiring_untrusted_redo_baseline() {
|
||||
let (root, path) = rolled_fixture();
|
||||
corrupt_baseline(&path);
|
||||
// Prosody never keeps a redo journal: it just appends to the native
|
||||
// file. When an external writer invalidated our baseline, behave the
|
||||
// same way instead of blocking the archive. A live append retires the
|
||||
// untrusted journal, adopts the synced native as authoritative, writes
|
||||
// the record, and recreates a fresh journal on the new baseline.
|
||||
let id = append_new(
|
||||
root.path(),
|
||||
"example.org",
|
||||
Some("alice"),
|
||||
"archive",
|
||||
Entry {
|
||||
id: "post-corruption".into(),
|
||||
when: 100000.,
|
||||
peer: String::new(),
|
||||
xml: "<message><body>new</body></message>".into(),
|
||||
},
|
||||
)
|
||||
.expect("live append must recover by retiring the untrusted baseline");
|
||||
assert_eq!(id, "post-corruption");
|
||||
// A fresh journal on the current native length is recreated.
|
||||
assert!(path.with_extension("rredo").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn untrusted_redo_baseline_is_retired_once_and_recovers() {
|
||||
let (root, path) = rolled_fixture();
|
||||
corrupt_baseline(&path);
|
||||
let make_entry = || Entry {
|
||||
id: "post-corruption".into(),
|
||||
when: 100000.,
|
||||
peer: String::new(),
|
||||
xml: "<message><body>new</body></message>".into(),
|
||||
};
|
||||
// The first access performs the full journal read+validation, then
|
||||
// retires the untrusted journal exactly once.
|
||||
append_new(
|
||||
root.path(),
|
||||
"example.org",
|
||||
Some("alice"),
|
||||
"archive",
|
||||
make_entry(),
|
||||
)
|
||||
.expect("first append retires the untrusted journal");
|
||||
// Subsequent appends are now fast and succeed on a healthy journal.
|
||||
for _ in 0..20 {
|
||||
append_new(
|
||||
root.path(),
|
||||
"example.org",
|
||||
Some("alice"),
|
||||
"archive",
|
||||
make_entry(),
|
||||
)
|
||||
.expect("archive stays writable after recovery");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offline_checkpoint_retires_untrusted_redo_baseline() {
|
||||
let (root, path) = rolled_fixture();
|
||||
let base = corrupt_baseline(&path);
|
||||
assert!(base > 0);
|
||||
assert!(path.with_extension("rredo").exists());
|
||||
// An external rewrite removed the trusted baseline; an explicit offline
|
||||
// handoff accepts the current synced native as authoritative and retires
|
||||
// the untrusted journal so the archive becomes writable again.
|
||||
let retired = checkpoint_all(root.path()).unwrap();
|
||||
assert_eq!(retired, 1);
|
||||
assert!(
|
||||
!path.with_extension("rredo").exists(),
|
||||
"offline checkpoint must retire the untrusted journal"
|
||||
);
|
||||
// The archive accepts new appends: with the journal retired the current
|
||||
// (now authoritative) native length becomes the fresh baseline.
|
||||
let id = append_new(
|
||||
root.path(),
|
||||
"example.org",
|
||||
Some("alice"),
|
||||
"archive",
|
||||
Entry {
|
||||
id: "after-handoff".into(),
|
||||
when: 200000.,
|
||||
peer: String::new(),
|
||||
xml: "<message><body>new</body></message>".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(id, "after-handoff");
|
||||
// The original baseline tail was externally rewritten, so the native
|
||||
// holds whatever the external writer put there; checkpoint only retires
|
||||
// the journal (handoff), it does not try to re-encode damaged records.
|
||||
// A fresh valid journal is (re)created on the next append.
|
||||
assert!(path.with_extension("rredo").exists());
|
||||
}
|
||||
}
|
||||
+193
-6
@@ -1,6 +1,17 @@
|
||||
//! Write buffering matching the supplied Prosody mod_csi_simple importance policy.
|
||||
use crate::{protocol, wire::escape};
|
||||
use std::time::{Duration, Instant};
|
||||
/// Runtime defaults: visible encrypted chat and delivery/read confirmation must
|
||||
/// not remain asleep merely because a client omitted a fallback body or EME.
|
||||
/// An explicit csi_important_payloads list replaces these defaults.
|
||||
pub const DEFAULT_IMPORTANT_PAYLOADS: &[&str] = &[
|
||||
"{eu.siacs.conversations.axolotl}encrypted",
|
||||
"{urn:xmpp:omemo:2}encrypted",
|
||||
"{urn:xmpp:receipts}received",
|
||||
"{urn:xmpp:chat-markers:0}received",
|
||||
"{urn:xmpp:chat-markers:0}displayed",
|
||||
"{urn:xmpp:chat-markers:0}acknowledged",
|
||||
];
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Buffer {
|
||||
inactive: bool,
|
||||
@@ -11,7 +22,67 @@ pub struct Buffer {
|
||||
pub resume_delay: Duration,
|
||||
pub important_payloads: Vec<String>,
|
||||
}
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct DurableBuffer {
|
||||
inactive: bool,
|
||||
queue: Vec<String>,
|
||||
limit: usize,
|
||||
resume_delay_ms: u64,
|
||||
important_payloads: Vec<String>,
|
||||
}
|
||||
/// Output awaiting persistence. Rollback reuses the drained queue without cloning it.
|
||||
pub(crate) struct PendingOutput {
|
||||
output: Vec<String>,
|
||||
previous: Option<QueueChange>,
|
||||
}
|
||||
struct QueueChange {
|
||||
queued: usize,
|
||||
bytes: usize,
|
||||
flushing_until: Option<Instant>,
|
||||
appended: bool,
|
||||
}
|
||||
impl PendingOutput {
|
||||
pub(crate) fn durable_changed(&self) -> bool {
|
||||
self.previous.is_some()
|
||||
}
|
||||
pub(crate) fn commit(self) -> Vec<String> {
|
||||
self.output
|
||||
}
|
||||
pub(crate) fn rollback(mut self, buffer: &mut Buffer) {
|
||||
if let Some(previous) = self.previous {
|
||||
if previous.appended {
|
||||
buffer.queue.pop();
|
||||
} else {
|
||||
self.output.truncate(previous.queued);
|
||||
buffer.queue = self.output;
|
||||
}
|
||||
buffer.bytes = previous.bytes;
|
||||
buffer.flushing_until = previous.flushing_until;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Buffer {
|
||||
pub(crate) fn durable(&self) -> DurableBuffer {
|
||||
DurableBuffer {
|
||||
inactive: self.inactive,
|
||||
queue: self.queue.clone(),
|
||||
limit: self.limit,
|
||||
resume_delay_ms: self.resume_delay.as_millis().min(u64::MAX as u128) as u64,
|
||||
important_payloads: self.important_payloads.clone(),
|
||||
}
|
||||
}
|
||||
pub(crate) fn from_durable(value: DurableBuffer) -> Self {
|
||||
Self {
|
||||
inactive: value.inactive,
|
||||
flushing_until: None,
|
||||
bytes: value.queue.iter().map(String::len).sum(),
|
||||
queue: value.queue,
|
||||
limit: value.limit,
|
||||
resume_delay: Duration::from_millis(value.resume_delay_ms),
|
||||
important_payloads: value.important_payloads,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure(&mut self, limit: usize, delay: Duration, payloads: Vec<String>) {
|
||||
self.limit = limit.max(1);
|
||||
self.resume_delay = delay;
|
||||
@@ -34,17 +105,47 @@ impl Buffer {
|
||||
}
|
||||
/// Client input flushes held output without declaring the client active.
|
||||
pub fn activity(&mut self, now: Instant) -> Vec<String> {
|
||||
self.pending_activity(now).commit()
|
||||
}
|
||||
pub(crate) fn pending_activity(&mut self, now: Instant) -> PendingOutput {
|
||||
if self.inactive && !self.queue.is_empty() {
|
||||
let previous = self.queue_change(false);
|
||||
self.flushing_until = Some(now + self.resume_delay);
|
||||
self.drain()
|
||||
PendingOutput {
|
||||
output: self.drain(),
|
||||
previous: Some(previous),
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
PendingOutput {
|
||||
output: Vec::new(),
|
||||
previous: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn output(&mut self, xml: String, owner: &str, now: Instant) -> Vec<String> {
|
||||
if !self.inactive || self.flushing_until.is_some_and(|until| now < until) {
|
||||
return vec![xml];
|
||||
self.pending_output(xml, owner, now).commit()
|
||||
}
|
||||
fn queue_change(&self, appended: bool) -> QueueChange {
|
||||
QueueChange {
|
||||
queued: self.queue.len(),
|
||||
bytes: self.bytes,
|
||||
flushing_until: self.flushing_until,
|
||||
appended,
|
||||
}
|
||||
}
|
||||
pub(crate) fn pending_output(
|
||||
&mut self,
|
||||
xml: String,
|
||||
owner: &str,
|
||||
now: Instant,
|
||||
) -> PendingOutput {
|
||||
if !self.inactive || self.flushing_until.is_some_and(|until| now < until) {
|
||||
return PendingOutput {
|
||||
output: vec![xml],
|
||||
previous: None,
|
||||
};
|
||||
}
|
||||
let mut previous = self.queue_change(false);
|
||||
self.flushing_until = None;
|
||||
if self.queue.len() >= self.limit.max(1)
|
||||
|| self.bytes.saturating_add(xml.len()) > 4 * 1024 * 1024
|
||||
@@ -53,12 +154,19 @@ impl Buffer {
|
||||
self.flushing_until = Some(now + self.resume_delay);
|
||||
let mut output = self.drain();
|
||||
output.push(xml);
|
||||
output
|
||||
PendingOutput {
|
||||
output,
|
||||
previous: (previous.queued > 0).then_some(previous),
|
||||
}
|
||||
} else {
|
||||
previous.appended = true;
|
||||
let xml = delayed(&xml, owner);
|
||||
self.bytes += xml.len();
|
||||
self.queue.push(xml);
|
||||
Vec::new()
|
||||
PendingOutput {
|
||||
output: Vec::new(),
|
||||
previous: Some(previous),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,3 +278,82 @@ fn delayed(xml: &str, owner: &str) -> String {
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod checkpoint_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_queue_changes_require_a_checkpoint() {
|
||||
let now = Instant::now();
|
||||
let mut buffer = Buffer::default();
|
||||
buffer.configure(2, Duration::from_secs(5), vec![]);
|
||||
assert!(!buffer.pending_activity(now).durable_changed());
|
||||
assert!(
|
||||
!buffer
|
||||
.pending_output("<presence/>".into(), "a@example", now)
|
||||
.durable_changed()
|
||||
);
|
||||
buffer.set_inactive(true);
|
||||
// An important stanza starts the transient grace period but changes no durable queue.
|
||||
let output = buffer.pending_output(
|
||||
"<message><body>hi</body></message>".into(),
|
||||
"a@example",
|
||||
now,
|
||||
);
|
||||
assert!(!output.durable_changed());
|
||||
assert_eq!(output.commit().len(), 1);
|
||||
assert!(
|
||||
!buffer
|
||||
.pending_output("<presence/>".into(), "a@example", now)
|
||||
.durable_changed()
|
||||
);
|
||||
let later = now + Duration::from_secs(6);
|
||||
let output = buffer.pending_output("<presence/>".into(), "a@example", later);
|
||||
assert!(output.durable_changed());
|
||||
assert!(output.commit().is_empty());
|
||||
let output = buffer.pending_activity(later);
|
||||
assert!(output.durable_changed());
|
||||
assert_eq!(output.commit().len(), 1);
|
||||
assert!(!buffer.pending_activity(later).durable_changed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_queue_checkpoints_restore_append_and_drain_without_copying_queue() {
|
||||
let now = Instant::now();
|
||||
let mut buffer = Buffer::default();
|
||||
buffer.configure(2, Duration::from_secs(5), vec![]);
|
||||
buffer.set_inactive(true);
|
||||
buffer.output("<presence id='held'/>".into(), "a@example", now);
|
||||
let before = serde_json::to_value(buffer.durable()).unwrap();
|
||||
let bytes = buffer.bytes;
|
||||
let until = buffer.flushing_until;
|
||||
for operation in 0..3 {
|
||||
let output = match operation {
|
||||
0 => buffer.pending_output("<presence id='new'/>".into(), "a@example", now),
|
||||
1 => buffer.pending_output(
|
||||
"<message><body>hi</body></message>".into(),
|
||||
"a@example",
|
||||
now,
|
||||
),
|
||||
_ => buffer.pending_activity(now),
|
||||
};
|
||||
assert!(output.durable_changed());
|
||||
output.rollback(&mut buffer);
|
||||
assert_eq!(serde_json::to_value(buffer.durable()).unwrap(), before);
|
||||
assert_eq!(buffer.bytes, bytes);
|
||||
assert_eq!(buffer.flushing_until, until);
|
||||
}
|
||||
// A successful retry still sends the held stanza before the new message.
|
||||
let output = buffer
|
||||
.pending_output(
|
||||
"<message><body>hi</body></message>".into(),
|
||||
"a@example",
|
||||
now,
|
||||
)
|
||||
.commit();
|
||||
assert_eq!(output.len(), 2);
|
||||
assert!(output[0].contains("id='held'"));
|
||||
assert!(output[1].contains("<body>hi</body>"));
|
||||
}
|
||||
}
|
||||
@@ -51,3 +51,5 @@ pub mod http_altconnect;
|
||||
pub mod object_store;
|
||||
|
||||
pub mod upload_migration;
|
||||
|
||||
pub mod session_store;
|
||||
+20
-1
@@ -12,6 +12,25 @@ async fn main() {
|
||||
async fn run() -> Result<()> {
|
||||
let args: Vec<_> = env::args().skip(1).collect();
|
||||
match args.first().map(String::as_str) {
|
||||
Some("storage-checkpoint") if args.len() == 2 => {
|
||||
let (c, domain) = rust_xmpp::server::settings_config(Path::new(&args[1]))?;
|
||||
let data = Path::new(
|
||||
c.option(&domain, "data_path")
|
||||
.and_then(|v| v.as_str())
|
||||
.context("data_path missing")?,
|
||||
);
|
||||
let _data_guard = rust_xmpp::data_lock::DataDirectoryGuard::acquire(data)?;
|
||||
let archives = rust_xmpp::archive::checkpoint_all(data)?;
|
||||
let sm_path = data
|
||||
.join(rust_xmpp::storage::encode_name(&domain))
|
||||
.join("rust_smacks");
|
||||
let sessions = if sm_path.try_exists()? {
|
||||
rust_xmpp::session_store::checkpoint_exports(&sm_path)?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
println!("Durable native archive handoff: {archives}; SM JSON exports: {sessions}");
|
||||
}
|
||||
Some("upload-migrate") if args.len() == 5 || args.len() == 6 => {
|
||||
anyhow::ensure!(
|
||||
args[3] == "--component" && (args.len() == 5 || args[5] == "--execute"),
|
||||
@@ -121,7 +140,7 @@ async fn run() -> Result<()> {
|
||||
println!("Account created: {}@{}", args[2], domain);
|
||||
}
|
||||
Some("--help") | None => println!(
|
||||
"rust-xmpp upload-migrate OLD_CONFIG NEW_CONFIG --component HOST [--execute]\nrust-xmpp check-config PATH\nrust-xmpp convert PATH json|yaml|toml OUTPUT\nrust-xmpp --version\n\nExperimental runtime: serve CONFIG --experimental\ncreate-user CONFIG USER --password-stdin\ncheck [CONFIG] (running server healthcheck)\nshell CONFIG COMMAND\n\nSTARTTLS, SCRAM/PLAIN, local messaging, roster, MAM, MUC, PEP/pubsub, stream management, push, federation and HTTP transports are implemented with limitations; see docs/COMPATIBILITY.md."
|
||||
"rust-xmpp upload-migrate OLD_CONFIG NEW_CONFIG --component HOST [--execute]\nrust-xmpp check-config PATH\nrust-xmpp convert PATH json|yaml|toml OUTPUT\nrust-xmpp --version\nrust-xmpp storage-checkpoint CONFIG (offline native handoff/downgrade)\n\nExperimental runtime: serve CONFIG --experimental\ncreate-user CONFIG USER --password-stdin\ncheck [CONFIG] (running server healthcheck)\nshell CONFIG COMMAND\n\nSTARTTLS, SCRAM/PLAIN, local messaging, roster, MAM, MUC, PEP/pubsub, stream management, push, federation and HTTP transports are implemented with limitations; see docs/COMPATIBILITY.md."
|
||||
),
|
||||
_ => bail!("invalid command; use --help"),
|
||||
}
|
||||
|
||||
+6
-10
@@ -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!(
|
||||
|
||||
+808
-94
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,319 @@
|
||||
//! The two native MUC component administration commands. Authority belongs to
|
||||
//! configured component administrators and is checked again on every step.
|
||||
use super::*;
|
||||
const COMMANDS: &str = "http://jabber.org/protocol/commands";
|
||||
const DESTROY: &str = "http://prosody.im/protocol/muc#destroy";
|
||||
const AFFILIATION: &str = "http://prosody.im/protocol/muc#set-affiliation";
|
||||
const INFO: &str = "http://jabber.org/protocol/disco#info";
|
||||
const ITEMS: &str = "http://jabber.org/protocol/disco#items";
|
||||
|
||||
pub(super) fn period(value: Option<&Value>) -> Option<f64> {
|
||||
let seconds = if let Some(number) = value.and_then(Value::as_f64) {
|
||||
number
|
||||
} else {
|
||||
let text = value?.as_str()?.trim();
|
||||
let split = text
|
||||
.find(|c: char| !(c.is_ascii_digit() || c == '.' || c == '+' || c == '-'))
|
||||
.unwrap_or(text.len());
|
||||
let number: f64 = text[..split].trim().parse().ok()?;
|
||||
let unit = text[split..].trim();
|
||||
number
|
||||
* match unit {
|
||||
"" | "s" | "second" | "seconds" => 1.0,
|
||||
"m" | "minute" | "minutes" => 60.0,
|
||||
"h" | "hour" | "hours" => 3600.0,
|
||||
"d" | "day" | "days" => 86400.0,
|
||||
"w" | "week" | "weeks" => 604800.0,
|
||||
_ => return None,
|
||||
}
|
||||
};
|
||||
(seconds.is_finite() && seconds >= 0.0).then_some(seconds)
|
||||
}
|
||||
pub(super) fn registration_form(nick: Option<&str>) -> String {
|
||||
format!(
|
||||
"<x xmlns='jabber:x:data' type='{}'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/muc#register</value></field><field var='muc#register_roomnick' type='text-single'>{}<required/></field></x>",
|
||||
if nick.is_some() { "result" } else { "form" },
|
||||
nick.map(|v| format!("<value>{}</value>", escape(v)))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
}
|
||||
fn command_reply(
|
||||
stanza: roxmltree::Node,
|
||||
node: &str,
|
||||
session: &str,
|
||||
status: &str,
|
||||
body: &str,
|
||||
) -> ModuleResult {
|
||||
ModuleResult::reply(iq_result(
|
||||
stanza,
|
||||
&format!(
|
||||
"<command xmlns='{COMMANDS}' node='{}' sessionid='{}' status='{status}'>{body}</command>",
|
||||
escape(node),
|
||||
escape(session)
|
||||
),
|
||||
))
|
||||
}
|
||||
fn form(muc: &Muc, node: &str) -> String {
|
||||
let fields = if node == DESTROY {
|
||||
let rooms = muc
|
||||
.rooms
|
||||
.keys()
|
||||
.map(|room| {
|
||||
format!(
|
||||
"<option label='{}'><value>{}</value></option>",
|
||||
escape(room),
|
||||
escape(room)
|
||||
)
|
||||
})
|
||||
.collect::<String>();
|
||||
format!(
|
||||
"<field var='rooms' type='list-multi' label='Rooms to destroy'><required/>{rooms}</field>"
|
||||
)
|
||||
} else {
|
||||
"<field var='room' type='jid-single' label='Room'><required/></field><field var='jid' type='jid-single' label='JID'><required/></field><field var='affiliation' type='list-single' label='Affiliation'><required/><option><value>owner</value></option><option><value>admin</value></option><option><value>member</value></option><option><value>none</value></option><option><value>outcast</value></option></field><field var='reason' type='text-single' label='Reason'/>".to_owned()
|
||||
};
|
||||
format!(
|
||||
"<actions execute='complete'><complete/></actions><x xmlns='jabber:x:data' type='form'><title>{}</title><field var='FORM_TYPE' type='hidden'><value>{node}</value></field>{fields}</x>",
|
||||
if node == DESTROY {
|
||||
"Destroy rooms"
|
||||
} else {
|
||||
"Set affiliation"
|
||||
}
|
||||
)
|
||||
}
|
||||
pub(super) fn handle(
|
||||
muc: &mut Muc,
|
||||
jid: &str,
|
||||
stanza: roxmltree::Node,
|
||||
) -> Result<Option<ModuleResult>> {
|
||||
if stanza.tag_name().name() != "iq" {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(query) = stanza.children().find(|n| n.is_element()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let ns = query.tag_name().namespace().unwrap_or("");
|
||||
let node = query.attribute("node").unwrap_or("");
|
||||
let is_admin = muc
|
||||
.component_admins
|
||||
.iter()
|
||||
.any(|admin| bare(admin) == bare(jid));
|
||||
if matches!(ns, INFO | ITEMS)
|
||||
&& (node == COMMANDS || matches!(node, DESTROY | AFFILIATION))
|
||||
&& stanza.attribute("type") == Some("get")
|
||||
{
|
||||
let body = if ns == ITEMS && node == COMMANDS {
|
||||
if is_admin {
|
||||
format!(
|
||||
"<item jid='{}' node='{DESTROY}' name='Destroy Rooms'/><item jid='{}' node='{AFFILIATION}' name='Set affiliation in room'/>",
|
||||
escape(&muc.domain),
|
||||
escape(&muc.domain)
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else if !is_admin && node != COMMANDS {
|
||||
return Ok(Some(ModuleResult::reply(iq_error(stanza, "forbidden"))));
|
||||
} else if ns == INFO {
|
||||
format!(
|
||||
"<identity category='automation' type='{}'/><feature var='{COMMANDS}'/><feature var='jabber:x:data'/>",
|
||||
if node == COMMANDS {
|
||||
"command-list"
|
||||
} else {
|
||||
"command-node"
|
||||
}
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
return Ok(Some(ModuleResult::reply(iq_result(
|
||||
stanza,
|
||||
&format!("<query xmlns='{ns}' node='{}'>{body}</query>", escape(node)),
|
||||
))));
|
||||
}
|
||||
if ns != COMMANDS || query.tag_name().name() != "command" {
|
||||
return Ok(None);
|
||||
}
|
||||
if !is_admin {
|
||||
return Ok(Some(ModuleResult::reply(iq_error(stanza, "forbidden"))));
|
||||
}
|
||||
if stanza.attribute("type") != Some("set") {
|
||||
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
|
||||
}
|
||||
if !matches!(node, DESTROY | AFFILIATION) {
|
||||
return Ok(Some(ModuleResult::reply(iq_error(
|
||||
stanza,
|
||||
"item-not-found",
|
||||
))));
|
||||
}
|
||||
muc.command_sessions
|
||||
.retain(|_, (_, _, started)| started.elapsed() < std::time::Duration::from_secs(600));
|
||||
let session = query.attribute("sessionid");
|
||||
let action = query.attribute("action").unwrap_or("execute");
|
||||
if session.is_none() {
|
||||
if action != "execute"
|
||||
|| query
|
||||
.children()
|
||||
.any(|n| n.has_tag_name(("jabber:x:data", "x")))
|
||||
{
|
||||
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
|
||||
}
|
||||
if muc.command_sessions.len() >= 1000 {
|
||||
return Ok(Some(ModuleResult::reply(iq_error(
|
||||
stanza,
|
||||
"resource-constraint",
|
||||
))));
|
||||
}
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
muc.command_sessions.insert(
|
||||
id.clone(),
|
||||
(
|
||||
bare(jid).to_owned(),
|
||||
node.to_owned(),
|
||||
std::time::Instant::now(),
|
||||
),
|
||||
);
|
||||
return Ok(Some(command_reply(
|
||||
stanza,
|
||||
node,
|
||||
&id,
|
||||
"executing",
|
||||
&form(muc, node),
|
||||
)));
|
||||
}
|
||||
let session = session.unwrap();
|
||||
if !muc
|
||||
.command_sessions
|
||||
.get(session)
|
||||
.is_some_and(|(owner, command, _)| owner == bare(jid) && command == node)
|
||||
{
|
||||
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
|
||||
}
|
||||
if action == "cancel" {
|
||||
muc.command_sessions.remove(session);
|
||||
return Ok(Some(command_reply(stanza, node, session, "canceled", "")));
|
||||
}
|
||||
if !matches!(action, "execute" | "complete") {
|
||||
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
|
||||
}
|
||||
let Some(data) = query
|
||||
.children()
|
||||
.find(|n| n.has_tag_name(("jabber:x:data", "x")) && n.attribute("type") == Some("submit"))
|
||||
else {
|
||||
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
|
||||
};
|
||||
let values = |name: &str| {
|
||||
data.children()
|
||||
.filter(|f| {
|
||||
f.has_tag_name(("jabber:x:data", "field")) && f.attribute("var") == Some(name)
|
||||
})
|
||||
.flat_map(|f| {
|
||||
f.children()
|
||||
.filter(|v| v.has_tag_name(("jabber:x:data", "value")))
|
||||
.filter_map(|v| v.text())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
if values("FORM_TYPE") != vec![node] {
|
||||
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
|
||||
}
|
||||
let mut out = ModuleResult::default();
|
||||
if node == DESTROY {
|
||||
let rooms = values("rooms");
|
||||
if rooms.is_empty()
|
||||
|| rooms
|
||||
.iter()
|
||||
.any(|room| bare(room) != *room || !room.ends_with(&format!("@{}", muc.domain)))
|
||||
{
|
||||
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
|
||||
}
|
||||
for name in rooms {
|
||||
muc.load(name)?;
|
||||
if !muc.rooms.contains_key(name) {
|
||||
continue;
|
||||
}
|
||||
// Only this internal, ACL-checked command bypasses room ownership.
|
||||
let packet = format!(
|
||||
"<iq type='set' to='{}'><query xmlns='{OWNER}'><destroy/></query></iq>",
|
||||
escape(name)
|
||||
);
|
||||
let doc = roxmltree::Document::parse(&packet)?;
|
||||
if let Some(mut destroyed) =
|
||||
muc.handle_inner_authorized(jid, doc.root_element(), true)?
|
||||
{
|
||||
out.deliveries.append(&mut destroyed.deliveries);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let rooms = values("room");
|
||||
let targets = values("jid");
|
||||
let affiliations = values("affiliation");
|
||||
if rooms.len() != 1 || targets.len() != 1 || affiliations.len() != 1 {
|
||||
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
|
||||
}
|
||||
let name = rooms[0];
|
||||
let target = targets[0];
|
||||
let affiliation = affiliations[0];
|
||||
if bare(name) != name
|
||||
|| !name.ends_with(&format!("@{}", muc.domain))
|
||||
|| crate::account_modules::normalize_bare_jid(bare(target)).is_err()
|
||||
|| !matches!(
|
||||
affiliation,
|
||||
"none" | "member" | "admin" | "owner" | "outcast"
|
||||
)
|
||||
{
|
||||
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
|
||||
}
|
||||
muc.load(name)?;
|
||||
let Some(room) = muc.rooms.get_mut(name) else {
|
||||
return Ok(Some(command_reply(
|
||||
stanza,
|
||||
node,
|
||||
session,
|
||||
"canceled",
|
||||
"<note type='error'>No such room</note>",
|
||||
)));
|
||||
};
|
||||
let reason = values("reason").first().copied().unwrap_or("");
|
||||
let packet = format!(
|
||||
"<iq type='set'><query xmlns='{ADMIN}'><item jid='{}' affiliation='{affiliation}'><reason>{}</reason></item></query></iq>",
|
||||
escape(&crate::account_modules::normalize_bare_jid(bare(target))?),
|
||||
escape(reason)
|
||||
);
|
||||
let doc = roxmltree::Document::parse(&packet)?;
|
||||
let iq = doc.root_element();
|
||||
let mut applied = ModuleResult::default();
|
||||
admin_with_authority(
|
||||
room,
|
||||
name,
|
||||
jid,
|
||||
iq,
|
||||
iq.children().find(|n| n.is_element()).unwrap(),
|
||||
&mut applied,
|
||||
true,
|
||||
);
|
||||
if applied.replies.iter().any(|p| p.contains("type='error'")) {
|
||||
return Ok(Some(command_reply(
|
||||
stanza,
|
||||
node,
|
||||
session,
|
||||
"canceled",
|
||||
"<note type='error'>Affiliation change failed</note>",
|
||||
)));
|
||||
}
|
||||
out.deliveries.append(&mut applied.deliveries);
|
||||
muc.save(name)?;
|
||||
}
|
||||
muc.command_sessions.remove(session);
|
||||
out.replies.append(
|
||||
&mut command_reply(
|
||||
stanza,
|
||||
node,
|
||||
session,
|
||||
"completed",
|
||||
"<note type='info'>Command completed</note>",
|
||||
)
|
||||
.replies,
|
||||
);
|
||||
Ok(Some(out))
|
||||
}
|
||||
+59
-5
@@ -66,14 +66,14 @@ pub struct RoomHistory {
|
||||
impl RoomHistory {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
capacity,
|
||||
capacity: capacity.min(4096),
|
||||
default_messages: 20,
|
||||
entries: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
pub fn set_capacity(&mut self, capacity: usize) {
|
||||
self.capacity = capacity;
|
||||
while self.entries.len() > capacity {
|
||||
self.capacity = capacity.min(4096);
|
||||
while self.entries.len() > self.capacity {
|
||||
self.entries.pop_front();
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ impl RoomHistory {
|
||||
/// Add one room broadcast, before per-recipient routing. Subject-only changes
|
||||
/// are sent separately on join and do not consume discussion history slots.
|
||||
pub fn push(&mut self, stanza: &str, timestamp: i64) {
|
||||
if self.capacity == 0 {
|
||||
if self.capacity == 0 || stanza.len() > 16 * 1024 {
|
||||
return;
|
||||
}
|
||||
let Ok(doc) = roxmltree::Document::parse(stanza) else {
|
||||
@@ -132,7 +132,7 @@ impl RoomHistory {
|
||||
}
|
||||
/// Restore a snapshot stanza that already carries its original delay.
|
||||
pub fn restore_last_message(&mut self, stanza: &str, timestamp: i64) {
|
||||
if self.capacity == 0 {
|
||||
if self.capacity == 0 || stanza.len() > 16 * 1024 {
|
||||
return;
|
||||
}
|
||||
let Ok(doc) = roxmltree::Document::parse(stanza) else {
|
||||
@@ -148,6 +148,60 @@ impl RoomHistory {
|
||||
});
|
||||
}
|
||||
|
||||
/// Lossless bounded crash snapshot, including original delay stamps.
|
||||
pub fn checkpoint(&self) -> crate::storage::LuaValue {
|
||||
use crate::storage::LuaValue as V;
|
||||
let mut value = V::table();
|
||||
for (i, entry) in self.entries.iter().enumerate() {
|
||||
let mut item = V::table();
|
||||
item.set("stanza", entry.stanza.clone().into());
|
||||
item.set("timestamp", V::Integer(entry.timestamp));
|
||||
value.set_key(V::Integer(i as i64 + 1), item);
|
||||
}
|
||||
value
|
||||
}
|
||||
pub fn restore_checkpoint(&mut self, value: &crate::storage::LuaValue) {
|
||||
self.entries.clear();
|
||||
// Lua pairs iteration is not sequence order, including numeric keys.
|
||||
let mut ordered = value
|
||||
.entries()
|
||||
.iter()
|
||||
.filter_map(|(key, item)| {
|
||||
key.as_i64()
|
||||
.filter(|index| *index > 0)
|
||||
.map(|index| (index, item))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
ordered.sort_by_key(|(index, _)| *index);
|
||||
for (_, item) in ordered.into_iter().take(4096) {
|
||||
let Some(stanza) = item
|
||||
.get("stanza")
|
||||
.and_then(crate::storage::LuaValue::as_str)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(timestamp) = item
|
||||
.get("timestamp")
|
||||
.and_then(crate::storage::LuaValue::as_i64)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if stanza.len() > 16 * 1024 {
|
||||
continue;
|
||||
}
|
||||
let Ok(doc) = roxmltree::Document::parse(stanza) else {
|
||||
continue;
|
||||
};
|
||||
if is_historic(doc.root_element()) {
|
||||
self.entries.push_back(Entry {
|
||||
stanza: stanza.into(),
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
self.set_capacity(self.capacity);
|
||||
}
|
||||
|
||||
pub fn replay(&self, _room: &str, to: &str, presence: &str, now: i64) -> Vec<String> {
|
||||
let request = HistoryRequest::parse(presence, now);
|
||||
let max = request.maxstanzas.unwrap_or_else(|| {
|
||||
|
||||
+137
-20
@@ -57,7 +57,7 @@ fn safe_payload(v: &V) -> String {
|
||||
if child
|
||||
.get("attr")
|
||||
.and_then(|a| field(a, "xmlns"))
|
||||
.is_some_and(|ns| ns != "jabber:client")
|
||||
.is_some_and(|ns| !matches!(ns, "" | "jabber:client"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -71,6 +71,17 @@ fn safe_payload(v: &V) -> String {
|
||||
}
|
||||
out
|
||||
}
|
||||
/// Keep the crash image transport-independent and exclude passwords/extensions.
|
||||
pub fn sanitize_presence_payload(payload: &str) -> String {
|
||||
if payload.len() > MAX_PRESENCE {
|
||||
return String::new();
|
||||
}
|
||||
let packet = format!("<presence>{payload}</presence>");
|
||||
let Ok(doc) = roxmltree::Document::parse(&packet) else {
|
||||
return String::new();
|
||||
};
|
||||
safe_payload(&crate::storage::stanza_value(doc.root_element()))
|
||||
}
|
||||
/// Decode Prosody's state store (and old frozen room maps) without trusting roles,
|
||||
/// primary-session pointers, or unvalidated stanza extensions.
|
||||
pub fn decode_room(room: &str, value: &V) -> Result<Vec<OccupantSnapshot>> {
|
||||
@@ -235,11 +246,29 @@ pub fn save_room_checkpoint(
|
||||
}
|
||||
|
||||
/// Dormant claims have a fixed restart deadline; probes never extend it.
|
||||
#[derive(Default)]
|
||||
#[derive(Clone)]
|
||||
pub struct PendingOccupants {
|
||||
rooms: BTreeMap<String, (u64, Vec<OccupantSnapshot>)>,
|
||||
deadlines: BTreeMap<(String, String), u64>,
|
||||
grace_seconds: u64,
|
||||
}
|
||||
impl Default for PendingOccupants {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rooms: BTreeMap::new(),
|
||||
deadlines: BTreeMap::new(),
|
||||
grace_seconds: RECOVERY_SECONDS,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl PendingOccupants {
|
||||
pub fn set_grace_seconds(&mut self, seconds: u64) {
|
||||
self.grace_seconds = seconds.clamp(1, 7 * 86400);
|
||||
}
|
||||
pub fn grace_seconds(&self) -> u64 {
|
||||
self.grace_seconds
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
room: String,
|
||||
@@ -252,8 +281,15 @@ impl PendingOccupants {
|
||||
<= MAX_OCCUPANTS,
|
||||
"pending recovery limit"
|
||||
);
|
||||
self.deadlines.retain(|(name, _), _| name != &room);
|
||||
for occupant in &occupants {
|
||||
self.deadlines.insert(
|
||||
(room.clone(), occupant.jid.clone()),
|
||||
now.saturating_add(self.grace_seconds),
|
||||
);
|
||||
}
|
||||
self.rooms
|
||||
.insert(room, (now.saturating_add(RECOVERY_SECONDS), occupants));
|
||||
.insert(room, (now.saturating_add(self.grace_seconds), occupants));
|
||||
Ok(())
|
||||
}
|
||||
/// Preserve a checkpoint's original deadline over repeated process restarts.
|
||||
@@ -264,7 +300,7 @@ impl PendingOccupants {
|
||||
saved_at: u64,
|
||||
now: u64,
|
||||
) -> Result<()> {
|
||||
if saved_at > now || saved_at.saturating_add(RECOVERY_SECONDS) <= now {
|
||||
if saved_at > now || saved_at.saturating_add(self.grace_seconds) <= now {
|
||||
return Ok(());
|
||||
}
|
||||
self.insert(room, occupants, saved_at)
|
||||
@@ -285,6 +321,8 @@ impl PendingOccupants {
|
||||
.iter()
|
||||
.position(|o| o.jid == authenticated_jid && o.nick == nick)?;
|
||||
let mut claim = occupants.remove(i);
|
||||
self.deadlines
|
||||
.remove(&(room.into(), authenticated_jid.into()));
|
||||
claim.jid = authenticated_jid.into();
|
||||
Some(claim)
|
||||
}
|
||||
@@ -306,10 +344,61 @@ impl PendingOccupants {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
pub fn checkpoint_members(&mut self, room: &str, now: u64) -> Vec<(u64, OccupantSnapshot)> {
|
||||
self.expire(now);
|
||||
self.rooms
|
||||
.get(room)
|
||||
.map(|(_, occupants)| {
|
||||
occupants
|
||||
.iter()
|
||||
.map(|o| {
|
||||
(
|
||||
self.deadlines
|
||||
.get(&(room.into(), o.jid.clone()))
|
||||
.copied()
|
||||
.unwrap_or(now)
|
||||
.saturating_sub(self.grace_seconds),
|
||||
o.clone(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
pub fn insert_checkpoint_members(
|
||||
&mut self,
|
||||
room: String,
|
||||
members: Vec<(u64, OccupantSnapshot)>,
|
||||
now: u64,
|
||||
) -> Result<()> {
|
||||
let members = members
|
||||
.into_iter()
|
||||
.filter(|(at, _)| {
|
||||
at.saturating_add(self.grace_seconds) > now
|
||||
&& at.saturating_add(self.grace_seconds) <= now.saturating_add(7 * 86400)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let Some(saved_at) = members.iter().map(|(at, _)| *at).min() else {
|
||||
return Ok(());
|
||||
};
|
||||
self.insert(
|
||||
room.clone(),
|
||||
members.iter().map(|(_, o)| o.clone()).collect(),
|
||||
saved_at,
|
||||
)?;
|
||||
for (at, o) in members {
|
||||
self.deadlines
|
||||
.insert((room.clone(), o.jid), at.saturating_add(self.grace_seconds));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn room_snapshot(&mut self, room: &str, now: u64) -> Option<(u64, Vec<OccupantSnapshot>)> {
|
||||
self.expire(now);
|
||||
self.rooms.get(room).map(|(deadline, occupants)| {
|
||||
(deadline.saturating_sub(RECOVERY_SECONDS), occupants.clone())
|
||||
(
|
||||
deadline.saturating_sub(self.grace_seconds),
|
||||
occupants.clone(),
|
||||
)
|
||||
})
|
||||
}
|
||||
/// Read-only membership probe; deadline-sensitive callers use contains_room.
|
||||
@@ -322,22 +411,31 @@ impl PendingOccupants {
|
||||
pub fn identities(&self, now: u64) -> Vec<(String, String)> {
|
||||
self.rooms
|
||||
.iter()
|
||||
.filter(|(_, (deadline, _))| *deadline > now)
|
||||
.flat_map(|(room, (_, occupants))| {
|
||||
occupants.iter().map(|o| (room.clone(), o.jid.clone()))
|
||||
occupants
|
||||
.iter()
|
||||
.filter(|o| {
|
||||
self.deadlines
|
||||
.get(&(room.clone(), o.jid.clone()))
|
||||
.is_some_and(|deadline| *deadline > now)
|
||||
})
|
||||
.map(|o| (room.clone(), o.jid.clone()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
/// A dormant nickname remains owned by its authenticated bare account until
|
||||
/// claim expiry; a reused resource belonging to another account cannot take it.
|
||||
pub fn reserved_by(&self, room: &str, nick: &str, now: u64) -> Option<String> {
|
||||
let (deadline, occupants) = self.rooms.get(room)?;
|
||||
if *deadline <= now {
|
||||
return None;
|
||||
}
|
||||
let (_, occupants) = self.rooms.get(room)?;
|
||||
occupants
|
||||
.iter()
|
||||
.find(|o| o.nick == nick)
|
||||
.find(|o| {
|
||||
o.nick == nick
|
||||
&& self
|
||||
.deadlines
|
||||
.get(&(room.into(), o.jid.clone()))
|
||||
.is_some_and(|deadline| *deadline > now)
|
||||
})
|
||||
.map(|o| bare(&o.jid).to_owned())
|
||||
}
|
||||
pub fn contains_room(&mut self, room: &str, now: u64) -> bool {
|
||||
@@ -346,17 +444,36 @@ impl PendingOccupants {
|
||||
.get(room)
|
||||
.is_some_and(|(_, occupants)| !occupants.is_empty())
|
||||
}
|
||||
pub fn remove_room(&mut self, room: &str) {
|
||||
self.rooms.remove(room);
|
||||
self.deadlines.retain(|(name, _), _| name != room);
|
||||
}
|
||||
pub fn remove_jid(&mut self, jid: &str) {
|
||||
self.deadlines.retain(|(_, identity), _| identity != jid);
|
||||
for (_, occupants) in self.rooms.values_mut() {
|
||||
occupants.retain(|o| o.jid != jid);
|
||||
}
|
||||
}
|
||||
pub fn expire(&mut self, now: u64) -> usize {
|
||||
let mut removed = 0;
|
||||
self.rooms.retain(|_, (deadline, occupants)| {
|
||||
if *deadline <= now {
|
||||
removed += occupants.len();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
let before = self.len();
|
||||
let deadlines = &self.deadlines;
|
||||
self.rooms.retain(|room, (deadline, occupants)| {
|
||||
occupants.retain(|o| {
|
||||
deadlines
|
||||
.get(&(room.clone(), o.jid.clone()))
|
||||
.is_some_and(|until| *until > now)
|
||||
});
|
||||
if let Some(first) = occupants
|
||||
.iter()
|
||||
.filter_map(|o| deadlines.get(&(room.clone(), o.jid.clone())))
|
||||
.min()
|
||||
{
|
||||
*deadline = *first;
|
||||
}
|
||||
!occupants.is_empty()
|
||||
});
|
||||
removed
|
||||
self.deadlines.retain(|_, until| *until > now);
|
||||
before - self.len()
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.rooms.values().map(|(_, v)| v.len()).sum()
|
||||
|
||||
@@ -2,6 +2,8 @@ use crate::wire::escape;
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ModuleResult {
|
||||
pub replies: Vec<String>,
|
||||
/// A final response after durable session revocation; never retained for resume.
|
||||
pub terminal_reply: bool,
|
||||
/// (destination full or bare JID, serialized stanza)
|
||||
pub deliveries: Vec<(String, String)>,
|
||||
}
|
||||
@@ -9,6 +11,7 @@ impl ModuleResult {
|
||||
pub fn reply(xml: String) -> Self {
|
||||
Self {
|
||||
replies: vec![xml],
|
||||
terminal_reply: false,
|
||||
deliveries: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1253,6 +1253,7 @@ pub fn on_presence(
|
||||
escape(&id)
|
||||
);
|
||||
Ok(ModuleResult {
|
||||
terminal_reply: false,
|
||||
replies: vec![],
|
||||
deliveries: vec![(jid.into(), query)],
|
||||
})
|
||||
@@ -1594,6 +1595,7 @@ pub fn on_remote_presence(
|
||||
vec![owner.into()],
|
||||
);
|
||||
Ok(ModuleResult {
|
||||
terminal_reply: false,
|
||||
replies: vec![],
|
||||
deliveries: vec![(
|
||||
verified_from.into(),
|
||||
|
||||
+508
-122
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,686 @@
|
||||
//! Bounded, per-session durable stream-management slots. Socket handles are never stored.
|
||||
//!
|
||||
//! Recovery is deliberately strict: malformed, oversized, or inconsistent committed
|
||||
//! slots fail startup instead of silently losing acknowledged session state. Uncommitted
|
||||
//! UUID temporary files are discarded; checksummed redo journals are authoritative.
|
||||
//! The readable JSON files are exports and are repaired from redo before loading.
|
||||
use crate::sessions::Session;
|
||||
use anyhow::{Result, ensure};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
const SLOT_LIMIT: usize = 8 * 1024 * 1024;
|
||||
const TOTAL_LIMIT: usize = 256 * 1024 * 1024;
|
||||
const SESSION_LIMIT: usize = 4096;
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct Record {
|
||||
version: u8,
|
||||
pub jid: String,
|
||||
pub owner: String,
|
||||
pub connection: String,
|
||||
pub client_id: Option<String>,
|
||||
pub push_identifier: Option<String>,
|
||||
pub first_push_sent: bool,
|
||||
pub available: bool,
|
||||
pub priority: i8,
|
||||
pub presence: Option<String>,
|
||||
pub carbons: bool,
|
||||
pub csi: crate::csi::DurableBuffer,
|
||||
pub roster_interested: bool,
|
||||
pub resume_id: String,
|
||||
pub handled: u32,
|
||||
pub sent: u32,
|
||||
pub acked: u32,
|
||||
pub ack_limit: usize,
|
||||
pub pending: std::collections::VecDeque<String>,
|
||||
pub deadline_ms: u64,
|
||||
pub timeout_ms: u64,
|
||||
}
|
||||
pub(crate) fn now_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
.min(u64::MAX as u128) as u64
|
||||
}
|
||||
impl Record {
|
||||
fn from_session(s: &Session) -> Result<Self> {
|
||||
ensure!(!s.closed && s.sm, "session is not resumable");
|
||||
let remaining = s
|
||||
.expires
|
||||
.map(|v| v.saturating_duration_since(Instant::now()))
|
||||
.unwrap_or(s.timeout);
|
||||
Ok(Self {
|
||||
version: 1,
|
||||
jid: s.jid.clone(),
|
||||
owner: s.owner.clone(),
|
||||
connection: s.connection.clone(),
|
||||
client_id: s.client_id.clone(),
|
||||
push_identifier: s.push_identifier.clone(),
|
||||
first_push_sent: s.first_push_sent,
|
||||
available: s.available,
|
||||
priority: s.priority,
|
||||
presence: s.presence.clone(),
|
||||
carbons: s.carbons,
|
||||
csi: s.csi.durable(),
|
||||
roster_interested: s.roster_interested,
|
||||
resume_id: s
|
||||
.resume_id
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing resume token"))?,
|
||||
handled: s.handled,
|
||||
sent: s.sent,
|
||||
acked: s.acked,
|
||||
ack_limit: s.ack_limit,
|
||||
pending: s.pending.clone(),
|
||||
deadline_ms: now_ms()
|
||||
.saturating_add(remaining.as_millis().min(u64::MAX as u128) as u64),
|
||||
timeout_ms: s.timeout.as_millis().min(u64::MAX as u128) as u64,
|
||||
})
|
||||
}
|
||||
pub fn session(self) -> Session {
|
||||
Session {
|
||||
jid: self.jid,
|
||||
owner: self.owner,
|
||||
connection: self.connection,
|
||||
client_id: self.client_id,
|
||||
push_identifier: self.push_identifier,
|
||||
first_push_sent: self.first_push_sent,
|
||||
sender: None,
|
||||
available: self.available,
|
||||
priority: self.priority,
|
||||
presence: self.presence,
|
||||
carbons: self.carbons,
|
||||
csi: crate::csi::Buffer::from_durable(self.csi),
|
||||
roster_interested: self.roster_interested,
|
||||
sm: true,
|
||||
resume_id: Some(self.resume_id),
|
||||
handled: self.handled,
|
||||
sent: self.sent,
|
||||
acked: self.acked,
|
||||
ack_limit: self.ack_limit,
|
||||
ack_requested: false,
|
||||
pending: self.pending,
|
||||
expires: Some(
|
||||
Instant::now() + Duration::from_millis(self.deadline_ms.saturating_sub(now_ms())),
|
||||
),
|
||||
timeout: Duration::from_millis(self.timeout_ms),
|
||||
closed: false,
|
||||
close_condition: None,
|
||||
cancel: Default::default(),
|
||||
durable: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Store {
|
||||
root: PathBuf,
|
||||
_lock: File,
|
||||
slots: Mutex<BTreeMap<String, (String, usize)>>,
|
||||
failed: std::sync::atomic::AtomicBool,
|
||||
retired: Mutex<Vec<String>>,
|
||||
journals: Mutex<BTreeMap<String, u64>>,
|
||||
}
|
||||
fn store(session: &Session) -> Result<Option<Arc<Store>>> {
|
||||
session
|
||||
.durable
|
||||
.as_ref()
|
||||
.map(|weak| {
|
||||
weak.upgrade()
|
||||
.ok_or_else(|| anyhow::anyhow!("durable session hub was dropped"))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
pub(crate) fn enable(s: &Session) -> Result<()> {
|
||||
if let Some(store) = store(s)?
|
||||
&& s.resume_id.is_some()
|
||||
{
|
||||
store.write(s, None)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn checkpoint(s: &Session) -> Result<()> {
|
||||
if let Some(store) = store(s)?
|
||||
&& s.resume_id.is_some()
|
||||
{
|
||||
store.write(s, Some(&s.connection))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn claim(old: &Session, new: &Session) -> Result<()> {
|
||||
if let Some(store) = store(old)?
|
||||
&& old.resume_id.is_some()
|
||||
{
|
||||
store.write(new, Some(&old.connection))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn remove(s: &Session) -> Result<()> {
|
||||
if let Some(store) = store(s)?
|
||||
&& let Some(id) = &s.resume_id
|
||||
{
|
||||
store.remove(id, &s.connection)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
impl Store {
|
||||
pub fn open(path: &Path) -> Result<(Arc<Self>, Vec<Record>)> {
|
||||
fs::create_dir_all(path)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(path, fs::Permissions::from_mode(0o700))?;
|
||||
}
|
||||
let mut options = OpenOptions::new();
|
||||
options.read(true).write(true).create(true).truncate(false);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.mode(0o600);
|
||||
}
|
||||
regular_or_missing(&path.join("store.lock"))?;
|
||||
let lock = options.open(path.join("store.lock"))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
lock.set_permissions(fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
lock.try_lock()
|
||||
.map_err(|error| anyhow::anyhow!("stream management store already in use: {error}"))?;
|
||||
let store = Arc::new(Self {
|
||||
root: path.into(),
|
||||
_lock: lock,
|
||||
slots: Default::default(),
|
||||
failed: std::sync::atomic::AtomicBool::new(false),
|
||||
retired: Default::default(),
|
||||
journals: Default::default(),
|
||||
});
|
||||
let mut recovered = vec![];
|
||||
let mut total = 0usize;
|
||||
let entries = fs::read_dir(path)?.collect::<std::io::Result<Vec<_>>>()?;
|
||||
// A crash before rename can leave an incomplete slot. Only canonical UUID
|
||||
// regular files belong to our writer; preserve unknown names and symlinks.
|
||||
for entry in &entries {
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if let Some(id) = name.strip_suffix(".tmp") {
|
||||
let id = id.strip_suffix(".wal").unwrap_or(id);
|
||||
let owned = safe_id(id).is_ok()
|
||||
&& uuid::Uuid::parse_str(id).is_ok_and(|uuid| uuid.to_string() == id);
|
||||
if owned && entry.file_type()?.is_file() {
|
||||
fs::remove_file(entry.path())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
File::open(path)?.sync_all()?;
|
||||
// Redo precedes JSON validation: the export may be stale or torn.
|
||||
for entry in &entries {
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if let Some(id) = name.strip_suffix(".wal") {
|
||||
safe_id(id)?;
|
||||
ensure!(
|
||||
entry.file_type()?.is_file(),
|
||||
"invalid stream management journal"
|
||||
);
|
||||
let snapshot = replay(&entry.path())?;
|
||||
// A process crash can leave a complete, checksummed append
|
||||
// only in page cache. Make its adoption durable before any
|
||||
// recovered slot is exposed to bootstrap or resume consumers.
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.open(entry.path())?
|
||||
.sync_data()?;
|
||||
{
|
||||
let mut journals = store.journals.lock().unwrap();
|
||||
journals.insert(id.to_string(), entry.metadata()?.len());
|
||||
ensure!(
|
||||
journals.len() <= SESSION_LIMIT + 64
|
||||
&& journals.values().sum::<u64>() <= JOURNAL_TOTAL_LIMIT,
|
||||
"stream management aggregate journal limit"
|
||||
);
|
||||
}
|
||||
if let Some(record) = &snapshot {
|
||||
validate_record(record, id)?;
|
||||
}
|
||||
store.export(
|
||||
id,
|
||||
snapshot
|
||||
.as_ref()
|
||||
.map(serde_json::to_vec)
|
||||
.transpose()?
|
||||
.as_deref(),
|
||||
)?;
|
||||
if snapshot.is_none() {
|
||||
store.retired.lock().unwrap().push(id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
let entries = fs::read_dir(path)?.collect::<std::io::Result<Vec<_>>>()?;
|
||||
for entry in entries {
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if !name.ends_with(".json") {
|
||||
continue;
|
||||
}
|
||||
ensure!(
|
||||
entry.file_type()?.is_file(),
|
||||
"invalid stream management slot"
|
||||
);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(entry.path(), fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
let len = entry.metadata()?.len() as usize;
|
||||
ensure!(len <= SLOT_LIMIT, "stream management slot too large");
|
||||
total = total.saturating_add(len);
|
||||
ensure!(
|
||||
total <= TOTAL_LIMIT && recovered.len() < SESSION_LIMIT,
|
||||
"stream management store limit"
|
||||
);
|
||||
let mut bytes = vec![];
|
||||
File::open(entry.path())?
|
||||
.take(SLOT_LIMIT as u64 + 1)
|
||||
.read_to_end(&mut bytes)?;
|
||||
let record: Record = serde_json::from_slice(&bytes)?;
|
||||
let id = name.strip_suffix(".json").unwrap();
|
||||
validate_record(&record, id)?;
|
||||
if record.deadline_ms <= now_ms() {
|
||||
store.commit(id, None)?;
|
||||
continue;
|
||||
}
|
||||
store
|
||||
.slots
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(record.resume_id.clone(), (record.connection.clone(), len));
|
||||
recovered.push(record);
|
||||
}
|
||||
File::open(path)?.sync_all()?;
|
||||
store.collect_retired()?;
|
||||
Ok((store, recovered))
|
||||
}
|
||||
fn write(&self, s: &Session, expected: Option<&str>) -> Result<()> {
|
||||
let record = Record::from_session(s)?;
|
||||
ensure!(
|
||||
record.pending.len() <= 4096,
|
||||
"stream management queue limit"
|
||||
);
|
||||
let bytes = serde_json::to_vec(&record)?;
|
||||
ensure!(bytes.len() <= SLOT_LIMIT, "stream management slot limit");
|
||||
let name = safe_id(&record.resume_id)?;
|
||||
let mut slots = self.slots.lock().unwrap();
|
||||
if let Some(expected) = expected {
|
||||
ensure!(
|
||||
slots
|
||||
.get(&record.resume_id)
|
||||
.is_some_and(|v| v.0 == expected),
|
||||
"retired stream transport"
|
||||
);
|
||||
} else {
|
||||
ensure!(
|
||||
!slots.contains_key(&record.resume_id)
|
||||
&& !self.retired.lock().unwrap().contains(&record.resume_id),
|
||||
"duplicate stream token"
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
slots.len() < SESSION_LIMIT || slots.contains_key(&record.resume_id),
|
||||
"stream management session limit"
|
||||
);
|
||||
let total: usize = slots.values().map(|v| v.1).sum();
|
||||
let previous = slots.get(&record.resume_id).map_or(0, |v| v.1);
|
||||
ensure!(
|
||||
total - previous + bytes.len() <= TOTAL_LIMIT,
|
||||
"stream management storage limit"
|
||||
);
|
||||
self.commit(name, Some(&bytes))?;
|
||||
slots.insert(record.resume_id, (record.connection, bytes.len()));
|
||||
Ok(())
|
||||
}
|
||||
fn remove(&self, id: &str, connection: &str) -> Result<()> {
|
||||
let mut slots = self.slots.lock().unwrap();
|
||||
if slots.get(id).is_none_or(|v| v.0 != connection) {
|
||||
return Ok(());
|
||||
}
|
||||
self.commit(safe_id(id)?, None)?;
|
||||
slots.remove(id);
|
||||
Ok(())
|
||||
}
|
||||
// A journal failure may have reached stable storage. Fence this store rather
|
||||
// than allowing an in-memory rollback to overwrite an uncertain commit.
|
||||
fn commit(&self, id: &str, bytes: Option<&[u8]>) -> Result<()> {
|
||||
use std::sync::atomic::Ordering;
|
||||
ensure!(
|
||||
!self.failed.load(Ordering::SeqCst),
|
||||
"stream management store failed"
|
||||
);
|
||||
let result = self.commit_inner(id, bytes);
|
||||
if result.is_err() {
|
||||
self.failed.store(true, Ordering::SeqCst);
|
||||
}
|
||||
result
|
||||
}
|
||||
fn commit_inner(&self, id: &str, bytes: Option<&[u8]>) -> Result<()> {
|
||||
let path = self.root.join(format!("{id}.wal"));
|
||||
regular_or_missing(&path)?;
|
||||
if !path.try_exists()? {
|
||||
// The baseline embeds the previously committed JSON, so recovery
|
||||
// never depends on an export modified without a subsequent fsync.
|
||||
let old = self.root.join(format!("{id}.json"));
|
||||
let baseline = if old.try_exists()? {
|
||||
regular_or_missing(&old)?;
|
||||
let mut baseline = Vec::new();
|
||||
File::open(old)?
|
||||
.take(SLOT_LIMIT as u64 + 1)
|
||||
.read_to_end(&mut baseline)?;
|
||||
ensure!(baseline.len() <= SLOT_LIMIT, "stream management slot limit");
|
||||
let record: Record = serde_json::from_slice(&baseline)?;
|
||||
validate_record(&record, id)?;
|
||||
Some(baseline)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let baseline_frame = frame(true, baseline.as_deref())?;
|
||||
if self.journals.lock().unwrap().values().sum::<u64>()
|
||||
+ JOURNAL_MAGIC.len() as u64
|
||||
+ baseline_frame.len() as u64
|
||||
> JOURNAL_TOTAL_LIMIT
|
||||
{
|
||||
self.compact_journals()?;
|
||||
}
|
||||
self.reset_journal(id, baseline.as_deref())?;
|
||||
}
|
||||
// Rotation is amortized over at least several small commits. Sync the
|
||||
// exported snapshot and its rename BEFORE replacing the redo history.
|
||||
let len = fs::metadata(&path)?.len();
|
||||
if len >= JOURNAL_ROTATE {
|
||||
let old = replay(&path)?;
|
||||
let old = old.as_ref().map(serde_json::to_vec).transpose()?;
|
||||
self.checkpoint_journal(id, old.as_deref())?;
|
||||
}
|
||||
let frame = frame(false, bytes)?;
|
||||
if self.journals.lock().unwrap().values().sum::<u64>() + frame.len() as u64
|
||||
> JOURNAL_TOTAL_LIMIT
|
||||
{
|
||||
// An aggregate cap prevents many small slots each growing a full
|
||||
// per-slot rotation budget. Compaction is rare and amortized over
|
||||
// hundreds of MiB of durable appends.
|
||||
self.compact_journals()?;
|
||||
}
|
||||
let mut journal = private_options().append(true).open(&path)?;
|
||||
let new_len = journal.metadata()?.len() + frame.len() as u64;
|
||||
journal.write_all(&frame)?;
|
||||
journal.sync_data()?;
|
||||
self.journals
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id.to_string(), new_len);
|
||||
// This remains readable for diagnostics and wire assertions, but only
|
||||
// the journal is authoritative after a crash.
|
||||
if self.export(id, bytes).is_err() {
|
||||
// The durable commit succeeded. Its caller must advance memory to
|
||||
// the same state rather than rolling it back. Fence future writes;
|
||||
// startup will reconstruct the export from redo.
|
||||
self.failed.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
return Ok(());
|
||||
}
|
||||
if bytes.is_none() {
|
||||
let mut retired = self.retired.lock().unwrap();
|
||||
retired.push(id.to_string());
|
||||
let collect = retired.len() >= 64;
|
||||
drop(retired);
|
||||
if collect && self.collect_retired().is_err() {
|
||||
self.failed.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn compact_journals(&self) -> Result<()> {
|
||||
let ids: Vec<String> = self.journals.lock().unwrap().keys().cloned().collect();
|
||||
for id in ids {
|
||||
let snapshot = replay(&self.root.join(format!("{id}.wal")))?;
|
||||
let snapshot = snapshot.as_ref().map(serde_json::to_vec).transpose()?;
|
||||
self.checkpoint_journal(&id, snapshot.as_deref())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn collect_retired(&self) -> Result<()> {
|
||||
let mut retired = self.retired.lock().unwrap();
|
||||
if retired.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
// Deletions become durable before their redo tombstones disappear.
|
||||
File::open(&self.root)?.sync_all()?;
|
||||
for id in retired.iter() {
|
||||
let path = self.root.join(format!("{id}.wal"));
|
||||
if path.try_exists()? {
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
self.journals.lock().unwrap().remove(id);
|
||||
}
|
||||
File::open(&self.root)?.sync_all()?;
|
||||
retired.clear();
|
||||
Ok(())
|
||||
}
|
||||
fn checkpoint_journal(&self, id: &str, bytes: Option<&[u8]>) -> Result<()> {
|
||||
self.export(id, bytes)?;
|
||||
if bytes.is_some() {
|
||||
File::open(self.root.join(format!("{id}.json")))?.sync_all()?;
|
||||
}
|
||||
File::open(&self.root)?.sync_all()?;
|
||||
self.reset_journal(id, bytes)
|
||||
}
|
||||
fn reset_journal(&self, id: &str, baseline: Option<&[u8]>) -> Result<()> {
|
||||
let tmp = self.root.join(format!("{id}.wal.tmp"));
|
||||
regular_or_missing(&tmp)?;
|
||||
let mut file = private_options().create(true).truncate(true).open(&tmp)?;
|
||||
private_permissions(&file)?;
|
||||
file.write_all(JOURNAL_MAGIC)?;
|
||||
file.write_all(&frame(true, baseline)?)?;
|
||||
file.sync_all()?;
|
||||
let len = file.metadata()?.len();
|
||||
fs::rename(tmp, self.root.join(format!("{id}.wal")))?;
|
||||
File::open(&self.root)?.sync_all()?;
|
||||
self.journals.lock().unwrap().insert(id.to_string(), len);
|
||||
Ok(())
|
||||
}
|
||||
fn export(&self, id: &str, bytes: Option<&[u8]>) -> Result<()> {
|
||||
let dest = self.root.join(format!("{id}.json"));
|
||||
if let Some(bytes) = bytes {
|
||||
let tmp = self.root.join(format!("{id}.tmp"));
|
||||
regular_or_missing(&tmp)?;
|
||||
let mut file = private_options().create(true).truncate(true).open(&tmp)?;
|
||||
private_permissions(&file)?;
|
||||
file.write_all(bytes)?;
|
||||
fs::rename(tmp, dest)?;
|
||||
} else if dest.try_exists()? {
|
||||
fs::remove_file(dest)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
/// Materialize the authoritative redo state as durable legacy JSON snapshots.
|
||||
///
|
||||
/// This is an offline handoff operation: it acquires the same exclusive store
|
||||
/// lock as a persistent Hub and fails while a live owner exists. Recovery and
|
||||
/// expiry run first. All current JSON data and directory entries become durable
|
||||
/// before any redo journal is removed. A failed checkpoint may leave redundant
|
||||
/// journals, but never removes the only durable copy of an acknowledged state.
|
||||
/// The caller should additionally hold its server-wide data-directory lock.
|
||||
pub fn checkpoint_exports(path: &Path) -> Result<usize> {
|
||||
let (store, records) = Store::open(path)?;
|
||||
for record in &records {
|
||||
let json = path.join(format!("{}.json", safe_id(&record.resume_id)?));
|
||||
regular_or_missing(&json)?;
|
||||
File::open(json)?.sync_all()?;
|
||||
}
|
||||
File::open(path)?.sync_all()?;
|
||||
let journals: Vec<String> = store.journals.lock().unwrap().keys().cloned().collect();
|
||||
for id in journals {
|
||||
let journal = path.join(format!("{id}.wal"));
|
||||
regular_or_missing(&journal)?;
|
||||
fs::remove_file(journal)?;
|
||||
}
|
||||
File::open(path)?.sync_all()?;
|
||||
Ok(records.len())
|
||||
}
|
||||
|
||||
fn safe_id(id: &str) -> Result<&str> {
|
||||
ensure!(
|
||||
!id.is_empty()
|
||||
&& id.len() <= 64
|
||||
&& id.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-'),
|
||||
"invalid resume token"
|
||||
);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
const JOURNAL_MAGIC: &[u8; 8] = b"XMPPSM01";
|
||||
const JOURNAL_ROTATE: u64 = 16 * 1024 * 1024;
|
||||
const JOURNAL_TOTAL_LIMIT: u64 = (TOTAL_LIMIT * 2) as u64;
|
||||
const FRAME_LIMIT: usize = SLOT_LIMIT + 1024;
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct JournalEntry<T> {
|
||||
baseline: bool,
|
||||
snapshot: Option<T>,
|
||||
}
|
||||
fn private_options() -> OpenOptions {
|
||||
let mut options = OpenOptions::new();
|
||||
options.write(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.mode(0o600);
|
||||
}
|
||||
options
|
||||
}
|
||||
fn private_permissions(file: &File) -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
file.set_permissions(fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn frame(baseline: bool, bytes: Option<&[u8]>) -> Result<Vec<u8>> {
|
||||
// The snapshot was already validated and serialized by write() or replay().
|
||||
// Copy its JSON directly into the frame rather than parsing and serializing
|
||||
// the entire pending queue again on every checkpoint.
|
||||
if let Some(bytes) = bytes {
|
||||
ensure!(bytes.len() <= SLOT_LIMIT, "stream management slot limit");
|
||||
}
|
||||
let mut payload = Vec::with_capacity(bytes.map_or(4, |snapshot| snapshot.len()) + 32);
|
||||
payload.extend_from_slice(if baseline {
|
||||
b"{\"baseline\":true,\"snapshot\":"
|
||||
} else {
|
||||
b"{\"baseline\":false,\"snapshot\":"
|
||||
});
|
||||
payload.extend_from_slice(bytes.unwrap_or(b"null"));
|
||||
payload.push(b'}');
|
||||
ensure!(
|
||||
payload.len() <= FRAME_LIMIT,
|
||||
"stream management journal frame limit"
|
||||
);
|
||||
let len = payload.len() as u32;
|
||||
let mut result = Vec::with_capacity(payload.len() + 40);
|
||||
result.extend_from_slice(&len.to_le_bytes());
|
||||
result.extend_from_slice(&(!len).to_le_bytes());
|
||||
result.extend_from_slice(&Sha256::digest(&payload));
|
||||
result.extend_from_slice(&payload);
|
||||
Ok(result)
|
||||
}
|
||||
fn replay(path: &Path) -> Result<Option<Record>> {
|
||||
let mut file = OpenOptions::new().read(true).write(true).open(path)?;
|
||||
private_permissions(&file)?;
|
||||
ensure!(
|
||||
file.metadata()?.len() <= JOURNAL_ROTATE + (FRAME_LIMIT + 40) as u64 + 8,
|
||||
"stream management journal limit"
|
||||
);
|
||||
let mut bytes = Vec::new();
|
||||
(&mut file)
|
||||
.take(JOURNAL_ROTATE + FRAME_LIMIT as u64 + 49)
|
||||
.read_to_end(&mut bytes)?;
|
||||
ensure!(
|
||||
bytes.starts_with(JOURNAL_MAGIC),
|
||||
"invalid stream management journal header"
|
||||
);
|
||||
let mut offset = 8;
|
||||
let mut first = true;
|
||||
let mut latest_payload = None;
|
||||
while offset < bytes.len() {
|
||||
if bytes.len() - offset < 8 {
|
||||
break;
|
||||
}
|
||||
let len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap());
|
||||
let complement = u32::from_le_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
|
||||
ensure!(
|
||||
len == !complement && len as usize <= FRAME_LIMIT,
|
||||
"corrupt stream management journal frame"
|
||||
);
|
||||
let end = offset + 40 + len as usize;
|
||||
if end > bytes.len() {
|
||||
break;
|
||||
}
|
||||
let payload = &bytes[offset + 40..end];
|
||||
ensure!(
|
||||
Sha256::digest(payload).as_slice() == &bytes[offset + 8..offset + 40],
|
||||
"stream management journal checksum mismatch"
|
||||
);
|
||||
// Historical snapshots were validated before their original commit.
|
||||
// Check every byte and the full JSON syntax again, without allocating
|
||||
// old pending queues or serializing superseded snapshots during replay.
|
||||
let entry: JournalEntry<serde::de::IgnoredAny> = serde_json::from_slice(payload)?;
|
||||
ensure!(
|
||||
entry.baseline == first,
|
||||
"invalid stream management journal baseline"
|
||||
);
|
||||
latest_payload = Some(payload);
|
||||
first = false;
|
||||
offset = end;
|
||||
}
|
||||
ensure!(!first, "incomplete stream management journal baseline");
|
||||
let latest: JournalEntry<Record> = serde_json::from_slice(latest_payload.unwrap())?;
|
||||
if let Some(record) = &latest.snapshot {
|
||||
let id = path.file_stem().unwrap().to_string_lossy();
|
||||
validate_record(record, &id)?;
|
||||
ensure!(
|
||||
serde_json::to_vec(record)?.len() <= SLOT_LIMIT,
|
||||
"stream management slot limit"
|
||||
);
|
||||
}
|
||||
if offset < bytes.len() {
|
||||
file.set_len(offset as u64)?;
|
||||
file.sync_all()?;
|
||||
}
|
||||
Ok(latest.snapshot)
|
||||
}
|
||||
fn validate_record(record: &Record, id: &str) -> Result<()> {
|
||||
ensure!(
|
||||
record.version == 1
|
||||
&& record.pending.len() <= 4096
|
||||
&& record.sent.wrapping_sub(record.acked) as usize == record.pending.len()
|
||||
&& crate::protocol::bare(&record.jid) == record.owner
|
||||
&& record.jid.contains('/'),
|
||||
"invalid durable session"
|
||||
);
|
||||
ensure!(safe_id(&record.resume_id)? == id, "invalid slot token");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn regular_or_missing(path: &Path) -> Result<()> {
|
||||
match fs::symlink_metadata(path) {
|
||||
Ok(metadata) => ensure!(metadata.is_file(), "invalid stream management file"),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+206
-33
@@ -35,8 +35,51 @@ pub struct Session {
|
||||
pub closed: bool,
|
||||
pub close_condition: Option<String>,
|
||||
pub cancel: Arc<tokio::sync::Notify>,
|
||||
pub(crate) durable: Option<std::sync::Weak<crate::session_store::Store>>,
|
||||
}
|
||||
impl Session {
|
||||
/// Persist state before acknowledging client input or exposing output to a transport.
|
||||
pub fn checkpoint(&self) -> Result<()> {
|
||||
crate::session_store::checkpoint(self)
|
||||
}
|
||||
pub fn mark_handled(&mut self) -> Result<()> {
|
||||
ensure!(!self.closed, "session closed");
|
||||
if !self.sm {
|
||||
return Ok(());
|
||||
}
|
||||
let previous = self.handled;
|
||||
self.handled = self.handled.wrapping_add(1);
|
||||
if let Err(error) = self.checkpoint() {
|
||||
self.handled = previous;
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn track_outbound(&mut self, xml: &str) -> Result<()> {
|
||||
ensure!(!self.closed, "session closed");
|
||||
if !self.sm {
|
||||
return Ok(());
|
||||
}
|
||||
ensure!(self.pending.len() < 4096, "stream management queue limit");
|
||||
// The byte bound applies to memory-only sessions too.
|
||||
ensure!(
|
||||
self.pending
|
||||
.iter()
|
||||
.map(String::len)
|
||||
.sum::<usize>()
|
||||
.saturating_add(xml.len())
|
||||
<= 8 * 1024 * 1024,
|
||||
"stream management byte limit"
|
||||
);
|
||||
self.sent = self.sent.wrapping_add(1);
|
||||
self.pending.push_back(xml.into());
|
||||
if let Err(error) = self.checkpoint() {
|
||||
self.sent = self.sent.wrapping_sub(1);
|
||||
self.pending.pop_back();
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn first_important_push(&mut self, normal_timeout: Duration) {
|
||||
if self.sender.is_none() && self.expires.is_some() && !self.first_push_sent {
|
||||
self.first_push_sent = true;
|
||||
@@ -45,25 +88,18 @@ impl Session {
|
||||
}
|
||||
pub fn transmit(&mut self, xml: String) -> Result<()> {
|
||||
ensure!(!self.closed, "session closed");
|
||||
if self.sm {
|
||||
ensure!(self.pending.len() < 4096, "stream management queue limit");
|
||||
}
|
||||
if let Some(tx) = &self.sender {
|
||||
if let Some(tx) = self.sender.clone() {
|
||||
let permit = tx
|
||||
.try_reserve()
|
||||
.map_err(|_| anyhow::anyhow!("recipient queue unavailable"))?;
|
||||
if self.sm {
|
||||
self.sent = self.sent.wrapping_add(1);
|
||||
self.pending.push_back(xml.clone());
|
||||
}
|
||||
self.track_outbound(&xml)?;
|
||||
permit.send(xml);
|
||||
} else {
|
||||
ensure!(
|
||||
self.sm && self.resume_id.is_some(),
|
||||
"recipient disconnected"
|
||||
);
|
||||
self.sent = self.sent.wrapping_add(1);
|
||||
self.pending.push_back(xml);
|
||||
self.track_outbound(&xml)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -79,9 +115,18 @@ impl Session {
|
||||
ensure!(!self.closed, "session closed");
|
||||
let n = h.wrapping_sub(self.acked) as usize;
|
||||
ensure!(n <= self.pending.len(), "invalid stream acknowledgement");
|
||||
if n == 0 {
|
||||
self.ack_requested = false;
|
||||
return Ok(());
|
||||
}
|
||||
let previous = self.clone();
|
||||
self.pending.drain(..n);
|
||||
self.acked = h;
|
||||
self.ack_requested = false;
|
||||
if let Err(error) = self.checkpoint() {
|
||||
*self = previous;
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn enable(&mut self, resume: bool) -> Result<String> {
|
||||
@@ -91,6 +136,11 @@ impl Session {
|
||||
if resume {
|
||||
self.resume_id = Some(uuid::Uuid::new_v4().to_string());
|
||||
}
|
||||
if let Err(error) = crate::session_store::enable(self) {
|
||||
self.sm = false;
|
||||
self.resume_id = None;
|
||||
return Err(error);
|
||||
}
|
||||
Ok(match &self.resume_id {
|
||||
Some(id) => format!(
|
||||
"<enabled xmlns='urn:xmpp:sm:3' id='{id}' resume='true' max='{}'/>",
|
||||
@@ -122,8 +172,62 @@ impl ConflictPolicy {
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Hub {
|
||||
sessions: Arc<Mutex<BTreeMap<String, SharedSession>>>,
|
||||
store: Option<Arc<crate::session_store::Store>>,
|
||||
}
|
||||
impl Hub {
|
||||
pub fn persistent(path: impl AsRef<std::path::Path>) -> Result<Self> {
|
||||
let (store, records) = crate::session_store::Store::open(path.as_ref())?;
|
||||
let mut sessions = BTreeMap::new();
|
||||
for record in records {
|
||||
let mut session = record.session();
|
||||
session.durable = Some(Arc::downgrade(&store));
|
||||
ensure!(
|
||||
!sessions.contains_key(&session.jid),
|
||||
"duplicate durable resource"
|
||||
);
|
||||
sessions.insert(session.jid.clone(), Arc::new(Mutex::new(session)));
|
||||
}
|
||||
Ok(Self {
|
||||
sessions: Arc::new(Mutex::new(sessions)),
|
||||
store: Some(store),
|
||||
})
|
||||
}
|
||||
/// Refresh only connected deadlines; disconnected deadlines never slide.
|
||||
pub fn checkpoint_live(&self) -> Result<()> {
|
||||
for session in self.all() {
|
||||
let state = session.lock().unwrap();
|
||||
if state.sender.is_some() && !state.closed {
|
||||
state.checkpoint()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retire in-flight transport handles and commit offline state before shutdown.
|
||||
pub fn freeze(&self) -> Result<()> {
|
||||
let mut map = self.sessions.lock().unwrap();
|
||||
for shared in map.values_mut() {
|
||||
let previous = shared.clone();
|
||||
let mut old = previous.lock().unwrap();
|
||||
if old.closed || !old.sm || old.resume_id.is_none() {
|
||||
continue;
|
||||
}
|
||||
let mut state = old.clone();
|
||||
state.sender = None;
|
||||
if state.expires.is_none() {
|
||||
state.expires = Some(Instant::now() + state.timeout);
|
||||
}
|
||||
state.connection = format!("shutdown-{}", uuid::Uuid::new_v4());
|
||||
state.cancel = Default::default();
|
||||
crate::session_store::claim(&old, &state)?;
|
||||
old.closed = true;
|
||||
old.sender = None;
|
||||
old.resume_id = None;
|
||||
old.expires = None;
|
||||
*shared = Arc::new(Mutex::new(state));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn bind(
|
||||
&self,
|
||||
jid: &str,
|
||||
@@ -150,9 +254,10 @@ impl Hub {
|
||||
match policy {
|
||||
ConflictPolicy::KickNew => anyhow::bail!("resource conflict"),
|
||||
ConflictPolicy::KickOld => {
|
||||
let old = map.remove(&jid).unwrap();
|
||||
let old = map.get(&jid).unwrap().clone();
|
||||
{
|
||||
let mut state = old.lock().unwrap();
|
||||
crate::session_store::remove(&state)?;
|
||||
state.closed = true;
|
||||
state.close_condition = Some("conflict".into());
|
||||
state.sender = None;
|
||||
@@ -160,6 +265,7 @@ impl Hub {
|
||||
state.expires = None;
|
||||
state.cancel.notify_one();
|
||||
}
|
||||
map.remove(&jid);
|
||||
replaced = Some(old);
|
||||
}
|
||||
ConflictPolicy::Increment => {
|
||||
@@ -208,22 +314,34 @@ impl Hub {
|
||||
closed: false,
|
||||
close_condition: None,
|
||||
cancel: Default::default(),
|
||||
durable: self.store.as_ref().map(Arc::downgrade),
|
||||
}));
|
||||
map.insert(jid, s.clone());
|
||||
Ok((s, replaced))
|
||||
}
|
||||
pub fn kick_owner_checked(&self, owner: &str) -> Result<()> {
|
||||
let mut map = self.sessions.lock().unwrap();
|
||||
let victims: Vec<_> = map
|
||||
.iter()
|
||||
.filter_map(|(jid, session)| {
|
||||
(session.lock().unwrap().owner == owner).then_some(jid.clone())
|
||||
})
|
||||
.collect();
|
||||
for jid in victims {
|
||||
let shared = map.get(&jid).unwrap().clone();
|
||||
let mut state = shared.lock().unwrap();
|
||||
// Do not let account deletion/recreation proceed if durable revocation failed.
|
||||
crate::session_store::remove(&state)?;
|
||||
state.resume_id = None;
|
||||
state.sender = None;
|
||||
state.closed = true;
|
||||
state.cancel.notify_one();
|
||||
map.remove(&jid);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn kick_owner(&self, owner: &str) {
|
||||
self.sessions.lock().unwrap().retain(|_, s| {
|
||||
let mut s = s.lock().unwrap();
|
||||
if s.owner == owner {
|
||||
s.sender = None;
|
||||
s.closed = true;
|
||||
s.cancel.notify_one();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
let _ = self.kick_owner_checked(owner);
|
||||
}
|
||||
pub fn get(&self, jid: &str) -> Option<SharedSession> {
|
||||
self.sessions.lock().unwrap().get(jid).cloned()
|
||||
@@ -231,13 +349,29 @@ impl Hub {
|
||||
pub fn all(&self) -> Vec<SharedSession> {
|
||||
self.sessions.lock().unwrap().values().cloned().collect()
|
||||
}
|
||||
/// Filter immutable resource keys before taking any per-session lock. A slow
|
||||
/// client from another account must not block this account's routing/carbons.
|
||||
pub fn for_owners(&self, owners: &[&str]) -> Vec<SharedSession> {
|
||||
self.sessions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|(jid, _)| owners.contains(&bare(jid)))
|
||||
.map(|(_, session)| session.clone())
|
||||
.collect()
|
||||
}
|
||||
pub fn deliver(&self, to: &str, xml: &str) -> usize {
|
||||
let roster_push = roxmltree::Document::parse(xml).ok().is_some_and(|d| {
|
||||
d.root_element()
|
||||
.attribute("id")
|
||||
.is_some_and(|id| id.starts_with("roster-"))
|
||||
});
|
||||
self.all()
|
||||
let targets = if to.contains('/') {
|
||||
self.get(to).into_iter().collect()
|
||||
} else {
|
||||
self.for_owners(&[to])
|
||||
};
|
||||
targets
|
||||
.into_iter()
|
||||
.filter(|s| {
|
||||
let mut s = s.lock().unwrap();
|
||||
@@ -269,7 +403,7 @@ impl Hub {
|
||||
let bare = to.split_once('/').map(|(bare, _)| bare).unwrap_or(to);
|
||||
let mut max = None;
|
||||
let mut out = vec![];
|
||||
for s in self.all() {
|
||||
for s in self.for_owners(&[bare]) {
|
||||
let p = {
|
||||
let state = s.lock().unwrap();
|
||||
if state.owner != bare || !state.available || state.priority < 0 {
|
||||
@@ -301,9 +435,21 @@ impl Hub {
|
||||
if !graceful && s.sm && s.resume_id.is_some() {
|
||||
s.csi.reset();
|
||||
s.sender = None;
|
||||
s.expires = Some(Instant::now() + s.timeout);
|
||||
// Repeated cleanup must not renew an already offline token.
|
||||
if s.expires.is_none() {
|
||||
s.expires = Some(Instant::now() + s.timeout);
|
||||
}
|
||||
if s.checkpoint().is_err() {
|
||||
return true;
|
||||
}
|
||||
true
|
||||
} else {
|
||||
if crate::session_store::remove(&s).is_err() {
|
||||
return true;
|
||||
}
|
||||
s.closed = true;
|
||||
s.resume_id = None;
|
||||
s.sender = None;
|
||||
removed.push(jid.clone());
|
||||
false
|
||||
}
|
||||
@@ -312,12 +458,16 @@ impl Hub {
|
||||
}
|
||||
pub fn expired(&self) -> Vec<String> {
|
||||
let mut out = vec![];
|
||||
self.sessions.lock().unwrap().retain(|jid, s| {
|
||||
if s.lock()
|
||||
.unwrap()
|
||||
.expires
|
||||
.is_some_and(|t| Instant::now() >= t)
|
||||
{
|
||||
self.sessions.lock().unwrap().retain(|jid, session| {
|
||||
let mut state = session.lock().unwrap();
|
||||
if state.expires.is_some_and(|t| Instant::now() >= t) {
|
||||
if crate::session_store::remove(&state).is_err() {
|
||||
return true;
|
||||
}
|
||||
state.closed = true;
|
||||
state.resume_id = None;
|
||||
state.sender = None;
|
||||
state.cancel.notify_one();
|
||||
out.push(jid.clone());
|
||||
false
|
||||
} else {
|
||||
@@ -340,7 +490,12 @@ impl Hub {
|
||||
.values()
|
||||
.find(|session| {
|
||||
let state = session.lock().unwrap();
|
||||
!state.closed && state.owner == owner && state.resume_id.as_deref() == Some(id)
|
||||
!state.closed
|
||||
&& state
|
||||
.expires
|
||||
.is_none_or(|deadline| deadline > Instant::now())
|
||||
&& state.owner == owner
|
||||
&& state.resume_id.as_deref() == Some(id)
|
||||
})
|
||||
.cloned();
|
||||
if let Some(previous) = candidate {
|
||||
@@ -351,13 +506,16 @@ impl Hub {
|
||||
// Keep a retired transport's in-flight operations isolated from the
|
||||
// resumed queue, acknowledgements and CSI state.
|
||||
let mut state = old.clone();
|
||||
state.acknowledge(h)?;
|
||||
state.pending.drain(..count);
|
||||
state.acked = h;
|
||||
state.ack_requested = false;
|
||||
state.cancel = Default::default();
|
||||
state.csi.reset();
|
||||
state.first_push_sent = false;
|
||||
state.sender = Some(sender);
|
||||
state.connection = connection.into();
|
||||
state.expires = None;
|
||||
crate::session_store::claim(&old, &state)?;
|
||||
let mut replay = vec![format!(
|
||||
"<resumed xmlns='urn:xmpp:sm:3' previd='{id}' h='{}'/>",
|
||||
state.handled
|
||||
@@ -383,6 +541,21 @@ impl Hub {
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn traffic_before_sm_enable_is_not_counted() {
|
||||
let hub = Hub::default();
|
||||
let (tx, _rx) = mpsc::channel(8);
|
||||
let session = hub
|
||||
.bind("a@x/r", "plain", tx, Duration::from_secs(30))
|
||||
.unwrap();
|
||||
let mut session = session.lock().unwrap();
|
||||
session.mark_handled().unwrap();
|
||||
session.track_outbound("plain reply").unwrap();
|
||||
session.enable(true).unwrap();
|
||||
assert_eq!((session.handled, session.sent), (0, 0));
|
||||
session.mark_handled().unwrap();
|
||||
assert_eq!(session.handled, 1);
|
||||
}
|
||||
#[test]
|
||||
fn resume_replays_only_unacked_and_checks_owner() {
|
||||
let h = Hub::default();
|
||||
let (tx, _rx) = mpsc::channel(8);
|
||||
|
||||
+1
-1
@@ -427,7 +427,7 @@ pub fn purge_user(root: &Path, host: &str, user: &str) -> Result<()> {
|
||||
if !ty.is_dir() {
|
||||
continue;
|
||||
}
|
||||
for extension in ["dat", "list", "lidx", "rappend"] {
|
||||
for extension in ["dat", "list", "lidx", "rappend", "rredo"] {
|
||||
let p = entry.path().join(format!("{user}.{extension}"));
|
||||
match fs::symlink_metadata(&p) {
|
||||
Ok(m) => {
|
||||
|
||||
@@ -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,358 @@
|
||||
//! Crash fixtures model independent loss of unsynced native writes. The redo
|
||||
//! sidecar is the durable commit source until a native checkpoint or handoff.
|
||||
use rust_xmpp::{archive, storage};
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
fn path(root: &Path) -> PathBuf {
|
||||
storage::path(root, "example.org", Some("alice"), "archive").with_extension("list")
|
||||
}
|
||||
fn entry(id: &str) -> archive::Entry {
|
||||
archive::Entry {
|
||||
id: id.into(),
|
||||
when: 42.,
|
||||
peer: "bob@example.org".into(),
|
||||
xml: "<message xmlns='jabber:client'><body>hello</body></message>".into(),
|
||||
}
|
||||
}
|
||||
fn append(root: &Path, id: &str) {
|
||||
archive::append_new(root, "example.org", Some("alice"), "archive", entry(id)).unwrap();
|
||||
}
|
||||
fn ids(root: &Path) -> Vec<String> {
|
||||
archive::load(root, "example.org", "alice")
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|e| e.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_redo_recovers_missing_and_partial_native_without_duplicates() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
append(t.path(), "one");
|
||||
append(t.path(), "two");
|
||||
let p = path(t.path());
|
||||
let expected = fs::read(&p).unwrap();
|
||||
for length in [0, 1, expected.len() / 2, expected.len() - 1] {
|
||||
fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&p)
|
||||
.unwrap()
|
||||
.set_len(length as u64)
|
||||
.unwrap();
|
||||
assert_eq!(ids(t.path()), ["one", "two"]);
|
||||
assert_eq!(fs::read(&p).unwrap(), expected);
|
||||
assert_eq!(ids(t.path()), ["one", "two"]);
|
||||
}
|
||||
fs::remove_file(&p).unwrap();
|
||||
assert_eq!(ids(t.path()), ["one", "two"]);
|
||||
assert_eq!(fs::read(&p).unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_final_frame_and_uncommitted_native_suffix_are_discarded() {
|
||||
for extra in [0, 1, 7, 20, 39, 40, 71, 72, 90] {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
append(t.path(), "one");
|
||||
let p = path(t.path());
|
||||
let committed = fs::read(&p).unwrap();
|
||||
let journal = p.with_extension("rredo");
|
||||
let committed_journal = fs::metadata(&journal).unwrap().len();
|
||||
append(t.path(), "two");
|
||||
// Construct a crash before the second frame was fully persisted.
|
||||
fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&journal)
|
||||
.unwrap()
|
||||
.set_len(committed_journal + extra)
|
||||
.unwrap();
|
||||
assert_eq!(ids(t.path()), ["one"]);
|
||||
assert_eq!(fs::read(&p).unwrap(), committed);
|
||||
assert_eq!(fs::metadata(&journal).unwrap().len(), committed_journal);
|
||||
append(t.path(), "three");
|
||||
assert_eq!(ids(t.path()), ["one", "three"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_checksum_corruption_fails_without_modifying_native_or_journal() {
|
||||
for offset in [8, 64, 96, 104, 136, 168] {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
append(t.path(), "one");
|
||||
let p = path(t.path());
|
||||
let native = fs::read(&p).unwrap();
|
||||
let journal = p.with_extension("rredo");
|
||||
let mut corrupt = fs::read(&journal).unwrap();
|
||||
corrupt[offset] ^= 1;
|
||||
fs::write(&journal, &corrupt).unwrap();
|
||||
assert!(archive::load(t.path(), "example.org", "alice").is_err());
|
||||
assert_eq!(fs::read(&p).unwrap(), native);
|
||||
assert_eq!(fs::read(&journal).unwrap(), corrupt);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_rotation_recovers_suffix_and_rewrites_never_resurrect_deleted_records() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
for i in 0..300 {
|
||||
append(t.path(), &format!("id{i}"));
|
||||
}
|
||||
let p = path(t.path());
|
||||
let journal = p.with_extension("rredo");
|
||||
let data = fs::read(&journal).unwrap();
|
||||
assert!(data.len() < 1024 * 1024);
|
||||
let baseline = u64::from_le_bytes(data[8..16].try_into().unwrap());
|
||||
assert!(baseline > 0);
|
||||
fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&p)
|
||||
.unwrap()
|
||||
.set_len(baseline)
|
||||
.unwrap();
|
||||
assert_eq!(ids(t.path()).len(), 300);
|
||||
archive::delete_ids(
|
||||
t.path(),
|
||||
"example.org",
|
||||
Some("alice"),
|
||||
"archive",
|
||||
&["id299".into()],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(ids(t.path()).len(), 299);
|
||||
append(t.path(), "after-delete");
|
||||
assert!(!ids(t.path()).contains(&"id299".into()));
|
||||
archive::save_store(
|
||||
t.path(),
|
||||
"example.org",
|
||||
Some("alice"),
|
||||
"archive",
|
||||
&[entry("replacement")],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(ids(t.path()), ["replacement"]);
|
||||
append(t.path(), "after-save");
|
||||
assert_eq!(ids(t.path()), ["replacement", "after-save"]);
|
||||
archive::expire_before(t.path(), "example.org", Some("alice"), "archive", 43.).unwrap();
|
||||
assert!(ids(t.path()).is_empty());
|
||||
append(t.path(), "after-expire");
|
||||
assert_eq!(ids(t.path()), ["after-expire"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_limit_rotates_before_frame_count_limit() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
for i in 0..20 {
|
||||
let mut e = entry(&format!("big{i}"));
|
||||
e.xml = format!("<message><body>{}</body></message>", "x".repeat(100_000));
|
||||
archive::append_new(t.path(), "example.org", Some("alice"), "archive", e).unwrap();
|
||||
}
|
||||
let data = fs::read(path(t.path()).with_extension("rredo")).unwrap();
|
||||
assert!(u64::from_le_bytes(data[8..16].try_into().unwrap()) > 0);
|
||||
assert!(data.len() < 1024 * 1024 + 110_000);
|
||||
assert_eq!(ids(t.path()).len(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_native_atomic_handoff_and_missing_baseline_never_drops_live_access() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
append(t.path(), "old");
|
||||
let p = path(t.path());
|
||||
archive::checkpoint_all(t.path()).unwrap();
|
||||
// An external writer replaces the native file in place (Prosody `list_store`
|
||||
// rewrite), invalidating the committed redo baseline. On first access the
|
||||
// untrusted journal is retired and the synced native is adopted as
|
||||
// authoritative — exactly what Prosody sees (it has no redo sidecar).
|
||||
storage::atomic_write(
|
||||
&p,
|
||||
b"item({name='message';attr={};key='native';when=42});\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(ids(t.path()), ["native"]);
|
||||
append(t.path(), "rust");
|
||||
assert_eq!(ids(t.path()), ["native", "rust"]);
|
||||
// A dirty, uncommitted native suffix is never surfaced.
|
||||
fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&p)
|
||||
.unwrap()
|
||||
.write_all(b"item(bad uncommitted);")
|
||||
.unwrap();
|
||||
assert_eq!(ids(t.path()), ["native", "rust"]);
|
||||
// Once the untrusted journal has been retired (native adopted as
|
||||
// authoritative), externally removing the archive yields the empty history
|
||||
// Prosody would return — never a panic or a hang.
|
||||
fs::remove_file(&p).unwrap();
|
||||
assert!(
|
||||
archive::load(t.path(), "example.org", "alice")
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_baseline_in_place_edits_are_rejected() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let p = path(t.path());
|
||||
fs::create_dir_all(p.parent().unwrap()).unwrap();
|
||||
let original = b"item({name='message';attr={};key='native';when=42});\n";
|
||||
fs::write(&p, original).unwrap();
|
||||
append(t.path(), "rust");
|
||||
let mut data = fs::read(&p).unwrap();
|
||||
data[5] ^= 1;
|
||||
fs::write(&p, &data).unwrap();
|
||||
assert!(archive::load(t.path(), "example.org", "alice").is_err());
|
||||
assert_eq!(fs::read(&p).unwrap(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redo_process_child() {
|
||||
let Some(root) = std::env::var_os("XMPP_ARCHIVE_REDO_CHILD") else {
|
||||
return;
|
||||
};
|
||||
let root = Path::new(&root);
|
||||
append(root, "accepted-before-kill");
|
||||
fs::write(root.join("ready"), b"ready").unwrap();
|
||||
loop {
|
||||
std::thread::park();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn killed_writer_retains_accepted_records_after_native_data_loss() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let mut child = Command::new(std::env::current_exe().unwrap())
|
||||
.args(["--exact", "redo_process_child", "--nocapture"])
|
||||
.env("XMPP_ARCHIVE_REDO_CHILD", t.path())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while !t.path().join("ready").exists() {
|
||||
if Instant::now() >= deadline {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
panic!("redo child not ready");
|
||||
}
|
||||
assert!(
|
||||
child.try_wait().unwrap().is_none(),
|
||||
"redo child exited early"
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
child.kill().unwrap();
|
||||
child.wait().unwrap();
|
||||
// Simulate loss of dirty native pages independently of the synced journal.
|
||||
fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(path(t.path()))
|
||||
.unwrap()
|
||||
.set_len(0)
|
||||
.unwrap();
|
||||
assert_eq!(ids(t.path()), ["accepted-before-kill"]);
|
||||
assert_eq!(ids(t.path()), ["accepted-before-kill"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn durable_handoff_allows_native_in_place_append_and_next_rust_append() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
append(t.path(), "rust-before");
|
||||
let p = path(t.path());
|
||||
assert_eq!(archive::checkpoint_all(t.path()).unwrap(), 1);
|
||||
assert!(!p.with_extension("rredo").exists());
|
||||
fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&p)
|
||||
.unwrap()
|
||||
.write_all(b"item({name='message';attr={};key='native';when=42});\n")
|
||||
.unwrap();
|
||||
assert_eq!(ids(t.path()), ["rust-before", "native"]);
|
||||
append(t.path(), "rust-after");
|
||||
assert_eq!(ids(t.path()), ["rust-before", "native", "rust-after"]);
|
||||
assert_eq!(archive::checkpoint_all(t.path()).unwrap(), 1);
|
||||
assert_eq!(archive::checkpoint_all(t.path()).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_rollback_marker_is_migrated_before_redo_append() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let p = path(t.path());
|
||||
fs::create_dir_all(p.parent().unwrap()).unwrap();
|
||||
let original = b"item({name='message';attr={};key='legacy';when=42});\n";
|
||||
fs::write(&p, [original.as_slice(), b"item(broken"].concat()).unwrap();
|
||||
fs::write(
|
||||
p.with_extension("rappend"),
|
||||
(original.len() as u64).to_le_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
append(t.path(), "redo");
|
||||
assert_eq!(ids(t.path()), ["legacy", "redo"]);
|
||||
assert!(!p.with_extension("rappend").exists());
|
||||
assert!(p.with_extension("rredo").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copied_archive_replays_committed_redo_despite_new_native_inode() {
|
||||
for native_baseline in [false, true] {
|
||||
for lose_entire_suffix in [false, true] {
|
||||
let source = tempfile::tempdir().unwrap();
|
||||
let source_path = path(source.path());
|
||||
if native_baseline {
|
||||
fs::create_dir_all(source_path.parent().unwrap()).unwrap();
|
||||
fs::write(
|
||||
&source_path,
|
||||
b"item({name='message';attr={};key='baseline';when=42});\n",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
append(source.path(), "accepted-one");
|
||||
append(source.path(), "accepted-two");
|
||||
let expected = fs::read(&source_path).unwrap();
|
||||
let redo = fs::read(source_path.with_extension("rredo")).unwrap();
|
||||
let baseline = u64::from_le_bytes(redo[8..16].try_into().unwrap());
|
||||
let restored = tempfile::tempdir().unwrap();
|
||||
let restored_path = path(restored.path());
|
||||
fs::create_dir_all(restored_path.parent().unwrap()).unwrap();
|
||||
fs::copy(&source_path, &restored_path).unwrap();
|
||||
fs::copy(
|
||||
source_path.with_extension("rredo"),
|
||||
restored_path.with_extension("rredo"),
|
||||
)
|
||||
.unwrap();
|
||||
let length = if lose_entire_suffix {
|
||||
baseline
|
||||
} else {
|
||||
expected.len() as u64 - 1
|
||||
};
|
||||
fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&restored_path)
|
||||
.unwrap()
|
||||
.set_len(length)
|
||||
.unwrap();
|
||||
let recovered = ids(restored.path());
|
||||
assert_eq!(recovered.last().unwrap(), "accepted-two");
|
||||
assert_eq!(recovered.len(), if native_baseline { 3 } else { 2 });
|
||||
assert_eq!(fs::read(&restored_path).unwrap(), expected);
|
||||
append(restored.path(), "after-restore");
|
||||
assert_eq!(ids(restored.path()).last().unwrap(), "after-restore");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn atomic_native_replace_without_handoff_cannot_discard_committed_frames() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
append(t.path(), "accepted");
|
||||
let p = path(t.path());
|
||||
storage::atomic_write(
|
||||
&p,
|
||||
b"item({name='message';attr={};key='replacement';when=42});\n",
|
||||
)
|
||||
.unwrap();
|
||||
// A replacement without handoff is not authority to discard committed WAL.
|
||||
assert_eq!(ids(t.path()), ["accepted"]);
|
||||
}
|
||||
@@ -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()
|
||||
Vendored
+74
@@ -0,0 +1,74 @@
|
||||
/* Test-only LD_PRELOAD shim: delay, then perform the real durable operation.
|
||||
* Scope is strictly the canonical fixture data root selected by the harness.
|
||||
* No host mount or durability settings are changed. Log contains no file data.
|
||||
* Exceptional XMPP_SYNC_FAIL_SUFFIX=.wal|.rredo injects EIO for matching
|
||||
* fixture regular files. Without that explicit fault mode all syncs are real.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static long long ns(void) {
|
||||
struct timespec t;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
return (long long)t.tv_sec * 1000000000LL + t.tv_nsec;
|
||||
}
|
||||
static int sync_call(int fd, const char *operation) {
|
||||
int (*real_sync)(int) = dlsym(RTLD_NEXT, operation);
|
||||
if (!real_sync) { errno = ENOSYS; return -1; }
|
||||
const char *prefix = getenv("XMPP_SLOW_SYNC_ROOT");
|
||||
const char *log = getenv("XMPP_SLOW_SYNC_LOG");
|
||||
char link[64], path[PATH_MAX];
|
||||
snprintf(link, sizeof link, "/proc/self/fd/%d", fd);
|
||||
ssize_t length = readlink(link, path, sizeof path - 1);
|
||||
if (!prefix || !log || length < 0) return real_sync(fd);
|
||||
path[length] = '\0';
|
||||
size_t size = strlen(prefix);
|
||||
if (strncmp(path, prefix, size) || (path[size] && path[size] != '/'))
|
||||
return real_sync(fd);
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) || (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)))
|
||||
return real_sync(fd);
|
||||
const char *delay = getenv("XMPP_SLOW_SYNC_MS");
|
||||
long ms = delay ? strtol(delay, NULL, 10) : 100;
|
||||
if (ms < 0 || ms > 10000) ms = 100;
|
||||
long long begin = ns();
|
||||
struct timespec wait = {.tv_sec = ms / 1000, .tv_nsec = (ms % 1000) * 1000000};
|
||||
while (nanosleep(&wait, &wait) && errno == EINTR) {}
|
||||
const char *suffix = getenv("XMPP_SYNC_FAIL_SUFFIX");
|
||||
int injected = 0;
|
||||
if (suffix && (!strcmp(suffix, ".wal") || !strcmp(suffix, ".rredo")) && S_ISREG(st.st_mode)) {
|
||||
size_t suffix_length = strlen(suffix);
|
||||
injected = (size_t)length >= suffix_length &&
|
||||
!strcmp(path + length - suffix_length, suffix);
|
||||
}
|
||||
int result;
|
||||
if (injected) { errno = EIO; result = -1; }
|
||||
else result = real_sync(fd);
|
||||
int saved_errno = errno;
|
||||
long long end = ns();
|
||||
/* One append write per operation avoids partial concurrent log records.
|
||||
* Paths are not logged: only a path-derived store category. */
|
||||
const char *store = strstr(path, "/rust_smacks") ? "sm" :
|
||||
strstr(path, "/archive") ? "mam" :
|
||||
strstr(path, "/pep") ? "pep" : "other";
|
||||
char row[512];
|
||||
int count = snprintf(row, sizeof row,
|
||||
"{\"op\":\"%s\",\"kind\":\"%s\",\"store\":\"%s\",\"begin_ns\":%lld,\"end_ns\":%lld,\"result\":%d,\"injected\":%s}\n",
|
||||
operation, S_ISDIR(st.st_mode) ? "directory" : "file", store, begin, end, result,
|
||||
injected ? "true" : "false");
|
||||
int output = open(log, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0600);
|
||||
if (output >= 0) { ssize_t ignored = write(output, row, count); (void)ignored; close(output); }
|
||||
errno = saved_errno;
|
||||
return result;
|
||||
}
|
||||
int fsync(int fd) { return sync_call(fd, "fsync"); }
|
||||
int fdatasync(int fd) { return sync_call(fd, "fdatasync"); }
|
||||
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail recovery journal syncs explicitly; never publish a false durable handoff.
|
||||
|
||||
Uses independent real TLS fixtures for .wal and .rredo so archive native inode
|
||||
identity remains genuine. SIGKILL leaves complete valid journals. The scoped
|
||||
test interposer returns EIO only for the selected suffix beneath fixture data.
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import ssl
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from muc_restart_network import stop
|
||||
from sm_restart_network import durable_slot
|
||||
from slow_storage_latency_network import CARBONS, MeasuredClient, check_shim_scope, iq, sync_rows
|
||||
from slow_storage_crash_network import committed_archive, start
|
||||
|
||||
|
||||
async def seed(root, binary, config, port, log, env):
|
||||
context = ssl.create_default_context(cafile=str(root/'cert.pem'))
|
||||
clients, server = [], None
|
||||
identifier = 'recovery-sync-body'
|
||||
try:
|
||||
server = await asyncio.to_thread(start, binary, config, port, log, env)
|
||||
for number in range(4):
|
||||
client = await MeasuredClient.connect(number, port, context, 2)
|
||||
clients.append(client)
|
||||
client.auto_ack = False
|
||||
await client.enable()
|
||||
await iq(client, f'carbons-{number}', f"<enable xmlns='{CARBONS}'/>")
|
||||
await client.stanza('<presence/>')
|
||||
await client.fence(f'ready-{number}')
|
||||
alice, bob, _, _ = clients
|
||||
await alice.stanza(f"<message to='{bob.jid}' type='chat' id='{identifier}'><body>{identifier}</body></message>")
|
||||
await alice.handled()
|
||||
for client, kind in zip(clients[1:], ('direct', 'sent', 'received')):
|
||||
await client.wait(lambda c=client,k=kind: c.arrivals[(identifier,k)], 'message/carbon before crash')
|
||||
server.send_signal(signal.SIGKILL)
|
||||
assert await asyncio.to_thread(server.wait, 10) == -signal.SIGKILL
|
||||
records = {client.token: durable_slot(root, client.token) for client in clients}
|
||||
assert len(records) == 4
|
||||
for client in clients[1:]:
|
||||
assert sum(identifier in xml for xml in records[client.token]['pending']) == 1
|
||||
archives = {p: committed_archive(p) for p in (root/'data'/'localhost'/'archive').glob('*.list')}
|
||||
assert len(archives) == 2
|
||||
assert all(sum(identifier in row for row in content.splitlines()) == 1 for content in archives.values())
|
||||
return records, archives
|
||||
finally:
|
||||
if server is not None and server.poll() is None:
|
||||
await asyncio.to_thread(stop, server)
|
||||
await asyncio.gather(*(c.close_raw() for c in clients), return_exceptions=True)
|
||||
|
||||
|
||||
def journal_bytes(root, suffix):
|
||||
return {p: p.read_bytes() for p in (root/'data').rglob('*'+suffix)}
|
||||
|
||||
|
||||
def assert_serve_rejects(binary, config, port, log, env):
|
||||
server = subprocess.Popen([str(binary), 'serve', str(config), '--experimental'],
|
||||
env=env, stdout=log, stderr=log)
|
||||
listened = False
|
||||
try:
|
||||
deadline = time.monotonic()+15
|
||||
while server.poll() is None and time.monotonic() < deadline:
|
||||
try:
|
||||
with socket.create_connection(('127.0.0.1', port), timeout=.05):
|
||||
listened = True
|
||||
break
|
||||
except OSError:
|
||||
time.sleep(.01)
|
||||
assert not listened, 'SM recovery sync failure exposed a listener'
|
||||
assert server.wait(timeout=5) != 0, 'SM recovery sync failure reported successful serve'
|
||||
finally:
|
||||
if server.poll() is None:
|
||||
server.kill()
|
||||
server.wait()
|
||||
|
||||
|
||||
def scope_fault(work, shim):
|
||||
root = work/'fault-scope'
|
||||
root.mkdir()
|
||||
code = """import errno,json,os,pathlib,sys
|
||||
root,outside,suffix=pathlib.Path(sys.argv[1]),pathlib.Path(sys.argv[2]),sys.argv[3]
|
||||
statuses=[]
|
||||
for path in (root/('inside'+suffix),root/'unmatched.txt',outside):
|
||||
with path.open('wb') as stream:
|
||||
stream.write(b'fixture');stream.flush()
|
||||
for sync in (os.fsync,os.fdatasync):
|
||||
try:sync(stream.fileno());statuses.append(0)
|
||||
except OSError as error:statuses.append(error.errno)
|
||||
assert statuses==[errno.EIO,errno.EIO,0,0,0,0],statuses
|
||||
"""
|
||||
for suffix in ('.wal', '.rredo'):
|
||||
log = work/('scope-'+suffix[1:]+'.jsonl')
|
||||
env = dict(os.environ, LD_PRELOAD=str(shim), XMPP_SLOW_SYNC_ROOT=str(root),
|
||||
XMPP_SLOW_SYNC_LOG=str(log), XMPP_SLOW_SYNC_MS='0', XMPP_SYNC_FAIL_SUFFIX=suffix)
|
||||
subprocess.run(['python3', '-c', code, str(root), str(work/('outside'+suffix)), suffix],
|
||||
env=env, check=True, capture_output=True)
|
||||
rows = sync_rows(log)
|
||||
assert len(rows) == 4 and sum(row['injected'] for row in rows) == 2
|
||||
|
||||
|
||||
def run_case(work, binary, shim, suffix):
|
||||
root = work/suffix[1:]
|
||||
root.mkdir()
|
||||
subprocess.run(['openssl', 'req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256',
|
||||
'-nodes', '-days', '1', '-subj', '/CN=localhost', '-addext', 'subjectAltName=DNS:localhost',
|
||||
'-keyout', str(root/'key.pem'), '-out', str(root/'cert.pem')], check=True, capture_output=True)
|
||||
with socket.socket() as probe:
|
||||
probe.bind(('127.0.0.1', 0))
|
||||
port = probe.getsockname()[1]
|
||||
config = root/'config.json'
|
||||
config.write_text(json.dumps({'global': {'data_path': str(root/'data'), 'interfaces': ['127.0.0.1'],
|
||||
'c2s_ports': [port], 's2s_ports': [], 'http_ports': [], 'https_ports': [],
|
||||
'modules_enabled': ['tls', 'saslauth', 'smacks', 'ping', 'mam', 'carbons'],
|
||||
'smacks_hibernation_time': 600, 'archive_expires_after': 'never', 'default_archive_policy': True,
|
||||
'ssl': {'key': str(root/'key.pem'), 'certificate': str(root/'cert.pem')}},
|
||||
'hosts': {'localhost': {'options': {'authentication': 'internal_hashed', 'password_hash': 'SHA-256'}}}}))
|
||||
for number in range(2):
|
||||
subprocess.run([str(binary), 'create-user', str(config), f'load-{number}', '--password-stdin'],
|
||||
input='load-test-password\n', text=True, check=True, capture_output=True)
|
||||
sync_log = root/'sync.jsonl'
|
||||
env = dict(os.environ, LD_PRELOAD=str(shim), XMPP_SLOW_SYNC_ROOT=str(root/'data'),
|
||||
XMPP_SLOW_SYNC_LOG=str(sync_log), XMPP_SLOW_SYNC_MS='0')
|
||||
env.pop('XMPP_SYNC_FAIL_SUFFIX', None)
|
||||
with (root/'server.log').open('w+') as log:
|
||||
records, archives = asyncio.run(seed(root, binary, config, port, log, env))
|
||||
journals = journal_bytes(root, suffix)
|
||||
assert journals, 'network fixture did not leave requested journals'
|
||||
fault_env = dict(env, XMPP_SYNC_FAIL_SUFFIX=suffix)
|
||||
before = len(sync_rows(sync_log))
|
||||
command = [str(binary), 'storage-checkpoint', str(config)]
|
||||
result = subprocess.run(command, env=fault_env, capture_output=True, text=True)
|
||||
assert result.returncode != 0, f'{suffix} recovery skipped journal sync and falsely reported successful handoff'
|
||||
assert 'Durable native archive handoff:' not in result.stdout, 'failed checkpoint advertised handoff success'
|
||||
attempts = [row for row in sync_rows(sync_log)[before:] if row.get('injected')]
|
||||
assert attempts and all(row['result'] == -1 for row in attempts), 'recovery did not attempt faulted journal sync'
|
||||
assert journal_bytes(root, suffix) == journals, 'failed checkpoint removed or changed authoritative journal'
|
||||
if suffix == '.wal':
|
||||
before_serve = len(sync_rows(sync_log))
|
||||
assert_serve_rejects(binary, config, port, log, fault_env)
|
||||
assert any(row.get('injected') and row['store'] == 'sm'
|
||||
for row in sync_rows(sync_log)[before_serve:]), 'serve failed without attempting SM recovery sync'
|
||||
assert journal_bytes(root, suffix) == journals, 'failed serve changed authoritative SM journals'
|
||||
subprocess.run(command, env=env, check=True, capture_output=True, text=True)
|
||||
assert not journal_bytes(root, '.wal') and not journal_bytes(root, '.rredo'), 'successful handoff left journals'
|
||||
for token, record in records.items():
|
||||
paths = list((root/'data').rglob(token+'.json'))
|
||||
assert len(paths) == 1
|
||||
exported = json.loads(paths[0].read_text())
|
||||
assert {k:v for k,v in exported.items() if k != 'deadline_ms'} == {
|
||||
k:v for k,v in record.items() if k != 'deadline_ms'}, 'handoff changed acknowledged SM state'
|
||||
assert all(path.read_text() == content for path, content in archives.items()), 'handoff lost acknowledged MAM body'
|
||||
return {'suffix': suffix, 'status': 'pass', 'checkpoint_rejected_sync_failure': True,
|
||||
'journals_preserved_on_failure': len(journals),
|
||||
'serve_rejected_before_listeners': suffix == '.wal',
|
||||
'clean_retry_preserved_sessions': len(records), 'clean_retry_preserved_archives': len(archives)}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--binary', default='target/release/rust-xmpp')
|
||||
parser.add_argument('--output', default='/tmp/journal-recovery-sync.json')
|
||||
parser.add_argument('--suffix', nargs='+', choices=['.wal', '.rredo'], default=['.wal', '.rredo'])
|
||||
args = parser.parse_args()
|
||||
report = {'status': 'running', 'cases': []}
|
||||
try:
|
||||
with tempfile.TemporaryDirectory(prefix='xmpp-journal-recovery-sync-') as directory:
|
||||
work = Path(directory)
|
||||
binary = work/'rust-xmpp'
|
||||
shutil.copy2(Path(args.binary).resolve(), binary)
|
||||
report['binary_sha256'] = hashlib.sha256(binary.read_bytes()).hexdigest()
|
||||
shim = work/'slow_sync.so'
|
||||
subprocess.run(['cc', '-shared', '-fPIC', '-O2', '-Wall', '-Wextra', '-Werror',
|
||||
str(Path(__file__).parent/'fixtures'/'slow_sync.c'), '-o', str(shim), '-ldl'], check=True)
|
||||
check_shim_scope(work, shim)
|
||||
scope_fault(work, shim)
|
||||
for suffix in args.suffix:
|
||||
report['cases'].append(run_case(work, binary, shim, suffix))
|
||||
report['status'] = 'pass'
|
||||
except BaseException as error:
|
||||
report.update(status='fail', failure=str(error))
|
||||
raise
|
||||
finally:
|
||||
Path(args.output).write_text(json.dumps(report, indent=2)+'\n')
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env python3
|
||||
"""A damaged MAM redo baseline must not take down the live message session.
|
||||
|
||||
Usage: python3 tests/mam_broken_baseline_network.py target/release/rust-xmpp
|
||||
|
||||
Reproduces the production symptom reported with Monal/Luma: the native archive at
|
||||
`data/<host>/archive/<user>.list` no longer matches the anchor recorded in its
|
||||
`.rredo` journal ("archive redo baseline changed in place"). On every message the
|
||||
previous build drained the session or stalled delivery for seconds, so the client
|
||||
timed out MAM and its message was delayed.
|
||||
|
||||
Prosody has no `.rredo` sidecar: it simply appends to the native `.list`. This
|
||||
test seeds an archive for `load-0`, corrupts its redo baseline offline to force
|
||||
that recovery error, then verifies that:
|
||||
* a fresh message is still routed load-0 -> load-1 quickly,
|
||||
* BOTH sessions survive (ping round-trip), no `connection-closed`,
|
||||
* the runtime self-repairs: on the first access it retires the untrusted
|
||||
journal, adopts the synced native as authoritative (exactly what Prosody
|
||||
would treat as the archive), and continues appending on a fresh journal
|
||||
without requiring an offline `storage-checkpoint` handoff,
|
||||
* MAM history reads succeed on the recovered archive.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
from _wire_client import Client, local
|
||||
|
||||
REDO_MAGIC = b"XARRED01"
|
||||
REDO_HEADER = 96
|
||||
BODY = "must-survive-broken-baseline"
|
||||
|
||||
|
||||
def start(binary, config, port, log):
|
||||
server = subprocess.Popen(
|
||||
[binary, "serve", str(config), "--experimental"], stdout=log, stderr=log
|
||||
)
|
||||
deadline = time.monotonic() + 60
|
||||
while time.monotonic() < deadline:
|
||||
assert server.poll() is None, "server stopped during startup"
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=0.1):
|
||||
return server
|
||||
except OSError:
|
||||
time.sleep(0.05)
|
||||
server.kill()
|
||||
server.wait()
|
||||
raise AssertionError("server did not start")
|
||||
|
||||
|
||||
def stop(server):
|
||||
server.terminate()
|
||||
try:
|
||||
code = server.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
server.kill()
|
||||
server.wait()
|
||||
raise AssertionError("SIGTERM did not complete orderly shutdown")
|
||||
assert code == 0, f"SIGTERM shutdown exited {code}"
|
||||
|
||||
|
||||
def corrupt_baseline(path):
|
||||
"""Force the `archive redo baseline changed in place` recovery error.
|
||||
|
||||
Rebuilds a rotated-style journal header leaving the native file untouched:
|
||||
the header records the native length as a non-empty baseline but stores a
|
||||
wrong anchor. `recover_redo` then rejects it exactly as when an external
|
||||
writer rewrote the native file in place after the journal rotated.
|
||||
"""
|
||||
native = path.read_bytes()
|
||||
length = len(native)
|
||||
assert length > 0, "seed produced an empty native MAM archive"
|
||||
header = bytearray(REDO_HEADER)
|
||||
header[:8] = REDO_MAGIC
|
||||
header[8:16] = length.to_bytes(8, "little")
|
||||
header[16:32] = b"\0" * 16 # dev/ino are only a cache identity, not validated
|
||||
tail = native[-min(length, 4096) :]
|
||||
real_anchor = hashlib.sha256(tail).digest()
|
||||
wrong = bytearray(real_anchor)
|
||||
wrong[0] ^= 0xFF
|
||||
header[32:64] = wrong
|
||||
header[64:96] = hashlib.sha256(header[:64]).digest()
|
||||
path.with_suffix(".rredo").write_bytes(header)
|
||||
assert hashlib.sha256(header[:64]).digest() == header[64:96], (
|
||||
"rebuilt header checksum"
|
||||
)
|
||||
assert header[32:64] != real_anchor, "anchor did not diverge from native"
|
||||
|
||||
|
||||
def run(binary):
|
||||
with tempfile.TemporaryDirectory(prefix="xmpp-mam-broken-") as tmp:
|
||||
root = Path(tmp)
|
||||
subprocess.run(
|
||||
[
|
||||
"openssl",
|
||||
"req",
|
||||
"-x509",
|
||||
"-newkey",
|
||||
"rsa:2048",
|
||||
"-nodes",
|
||||
"-days",
|
||||
"1",
|
||||
"-keyout",
|
||||
str(root / "key.pem"),
|
||||
"-out",
|
||||
str(root / "cert.pem"),
|
||||
"-subj",
|
||||
"/CN=localhost",
|
||||
"-addext",
|
||||
"subjectAltName=DNS:localhost",
|
||||
],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
with socket.socket() as probe:
|
||||
probe.bind(("127.0.0.1", 0))
|
||||
port = probe.getsockname()[1]
|
||||
config = root / "config.json"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"global": {
|
||||
"data_path": str(root / "data"),
|
||||
"interfaces": ["127.0.0.1"],
|
||||
"c2s_ports": [port],
|
||||
"s2s_ports": [],
|
||||
"http_ports": [],
|
||||
"https_ports": [],
|
||||
"modules_enabled": [
|
||||
"tls",
|
||||
"saslauth",
|
||||
"smacks",
|
||||
"ping",
|
||||
"mam",
|
||||
"carbons",
|
||||
],
|
||||
"smacks_hibernation_time": 600,
|
||||
"archive_expires_after": "never",
|
||||
"default_archive_policy": True,
|
||||
"ssl": {
|
||||
"key": str(root / "key.pem"),
|
||||
"certificate": str(root / "cert.pem"),
|
||||
},
|
||||
},
|
||||
"hosts": {
|
||||
"localhost": {
|
||||
"options": {
|
||||
"authentication": "internal_hashed",
|
||||
"password_hash": "SHA-256",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
for number in range(2):
|
||||
subprocess.run(
|
||||
[
|
||||
binary,
|
||||
"create-user",
|
||||
str(config),
|
||||
f"load-{number}",
|
||||
"--password-stdin",
|
||||
],
|
||||
input="load-test-password\n",
|
||||
text=True,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
with (root / "server.log").open("w+") as log:
|
||||
server = None
|
||||
try:
|
||||
# Seed: leave a real, valid MAM entry for load-0 so its archive
|
||||
# carries a genuine .list + .rredo pair with a non-zero baseline.
|
||||
server = start(binary, config, port, log)
|
||||
alice = Client(
|
||||
port, root / "cert.pem", "load-0", "load-test-password", "desktop"
|
||||
)
|
||||
bob = Client(
|
||||
port, root / "cert.pem", "load-1", "load-test-password", "desktop"
|
||||
)
|
||||
alice.available()
|
||||
bob.available()
|
||||
alice.send(
|
||||
f"<message to='{bob.jid}' type='chat' id='seed'><body>seed</body></message>"
|
||||
)
|
||||
bob.body("seed")
|
||||
alice.sync()
|
||||
stop(server)
|
||||
server = None
|
||||
for client in (alice, bob):
|
||||
client.close()
|
||||
finally:
|
||||
if server is not None and server.poll() is None:
|
||||
server.kill()
|
||||
server.wait()
|
||||
|
||||
# The sender localpart is percent-encoded on disk (`load-0` -> `load%2d0`).
|
||||
sender_archive = root / "data" / "localhost" / "archive" / "load%2d0.list"
|
||||
assert sender_archive.exists(), "seed did not create load-0 MAM archive"
|
||||
assert sender_archive.with_suffix(".rredo").exists(), (
|
||||
"seed did not attach load-0 redo journal"
|
||||
)
|
||||
corrupt_baseline(sender_archive)
|
||||
damaged_journal = sender_archive.with_suffix(".rredo").read_bytes()
|
||||
|
||||
with (root / "server.log").open("a") as log:
|
||||
server = None
|
||||
try:
|
||||
server = start(binary, config, port, log)
|
||||
alice = Client(
|
||||
port, root / "cert.pem", "load-0", "load-test-password", "desktop"
|
||||
)
|
||||
bob = Client(
|
||||
port, root / "cert.pem", "load-1", "load-test-password", "desktop"
|
||||
)
|
||||
alice.available()
|
||||
bob.available()
|
||||
# A damaged MAM baseline must not drop the dispatch: deliver and
|
||||
# keep both sessions alive.
|
||||
alice.send(
|
||||
f"<message to='{bob.jid}' type='chat' id='live'><body>{BODY}</body></message>"
|
||||
)
|
||||
bob.body(BODY)
|
||||
# Ping round-trips prove neither session was closed by the archive fault.
|
||||
alice.sync()
|
||||
bob.sync()
|
||||
alice.send(
|
||||
f"<message to='{bob.jid}' type='chat' id='second'><body>second</body></message>"
|
||||
)
|
||||
bob.body("second")
|
||||
alice.sync()
|
||||
bob.sync()
|
||||
finally:
|
||||
if server is not None and server.poll() is None:
|
||||
server.kill()
|
||||
server.wait()
|
||||
|
||||
log_text = (root / "server.log").read_text()
|
||||
assert not any(
|
||||
"connection-closed" in l and "protocol-or-transport-error" in l
|
||||
for l in log_text.splitlines()
|
||||
), "a session was closed by the archive fault"
|
||||
assert not any("session-error-label" in l for l in log_text.splitlines()), (
|
||||
"unhandled archive fault labelled a session error"
|
||||
)
|
||||
|
||||
# Prosody-compatible self-repair: the first access after the corruption
|
||||
# retired the untrusted journal and the archive was appended to on a fresh
|
||||
# journal. No `mam-archive-skipped` should have been logged because the
|
||||
# event delivery succeeded on the recovered native.
|
||||
assert not any("mam-archive-skipped" in l for l in log_text.splitlines())
|
||||
|
||||
# A fresh journal must have replaced the damaged one (the archive kept
|
||||
# accepting records). Its baseline now matches the current native.
|
||||
journal = sender_archive.with_suffix(".rredo")
|
||||
assert journal.exists(), "runtime did not recreate a fresh redo journal"
|
||||
assert journal.read_bytes() != damaged_journal, (
|
||||
"runtime kept the damaged journal instead of retiring it"
|
||||
)
|
||||
fresh = journal.read_bytes()
|
||||
assert len(fresh) >= REDO_HEADER
|
||||
assert fresh[:8] == REDO_MAGIC
|
||||
# The fresh journal is valid: its header digest checks out, so recovery
|
||||
# will accept it as the authoritative pairing with the current native.
|
||||
assert hashlib.sha256(fresh[:64]).digest() == fresh[64:96], (
|
||||
"recreated journal header is not self-consistent"
|
||||
)
|
||||
|
||||
# MAM history on the recovered, now-authoritative native must serve reads.
|
||||
phase3_offset = len((root / "server.log").read_text().splitlines())
|
||||
with (root / "server.log").open("a") as log:
|
||||
server = None
|
||||
try:
|
||||
server = start(binary, config, port, log)
|
||||
alice = Client(
|
||||
port, root / "cert.pem", "load-0", "load-test-password", "desktop"
|
||||
)
|
||||
bob = Client(
|
||||
port, root / "cert.pem", "load-1", "load-test-password", "desktop"
|
||||
)
|
||||
alice.available()
|
||||
bob.available()
|
||||
# A MAM query to load-0's own archive must serve history.
|
||||
alice.send(
|
||||
f"<iq type='set' id='mam' to='load-0@localhost'>"
|
||||
f"<query xmlns='urn:xmpp:mam:2'/>"
|
||||
f"<x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE'>"
|
||||
f"<value>urn:xmpp:mam:2</value></field></x></iq>"
|
||||
)
|
||||
result_iq = alice.iq("mam")
|
||||
assert (
|
||||
result_iq.find(
|
||||
".//{urn:ietf:params:xml:ns:xmpp-stanzas}internal-server-error"
|
||||
)
|
||||
is None
|
||||
), ET.tostring(result_iq)
|
||||
# A new message still archives cleanly on the recovered journal.
|
||||
alice.send(
|
||||
f"<message to='{bob.jid}' type='chat' id='post-repair'><body>post-repair</body></message>"
|
||||
)
|
||||
bob.body("post-repair")
|
||||
alice.sync()
|
||||
finally:
|
||||
if server is not None and server.poll() is None:
|
||||
server.kill()
|
||||
server.wait()
|
||||
|
||||
post_log = (root / "server.log").read_text().splitlines()[phase3_offset:]
|
||||
fresh_errors = [
|
||||
l for l in post_log if "archive redo baseline changed in place" in l
|
||||
]
|
||||
assert not fresh_errors, (
|
||||
"archive fault returned after self-repair:\n" + "\n".join(fresh_errors)
|
||||
)
|
||||
|
||||
# Offline handoff stays safe: checkpoint of an already-repaired archive is
|
||||
# idempotent (retires an absent journal) and still reports a handoff.
|
||||
result = subprocess.run(
|
||||
[binary, "storage-checkpoint", str(config)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
assert "Durable native archive handoff:" in result.stdout
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--binary", default="target/release/rust-xmpp")
|
||||
args = parser.parse_args()
|
||||
run(args.binary)
|
||||
print("PASS: damaged MAM redo baseline does not drop live message delivery")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user