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

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

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

Причины, изменение CSI policy, измерения и ограничения:
[docs/PERSONAL-MESSAGE-LATENCY.md](docs/PERSONAL-MESSAGE-LATENCY.md).
2026-10-21 11:20:09 +07:00

134 lines
5.0 KiB
Rust

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());
}