сообщения больше не выполняется полный 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).
147 lines
5.6 KiB
Rust
147 lines
5.6 KiB
Rust
use rust_xmpp::{archive, muc::Muc, protocol::ModuleResult, storage};
|
|
use std::{fs, path::Path, time::SystemTime};
|
|
|
|
fn send(muc: &mut Muc, xml: &str) -> ModuleResult {
|
|
let doc = roxmltree::Document::parse(xml).unwrap();
|
|
muc.handle("alice@test/a", doc.root_element())
|
|
.unwrap()
|
|
.unwrap()
|
|
}
|
|
|
|
fn room(muc: &mut Muc, name: &str) {
|
|
send(
|
|
muc,
|
|
&format!(
|
|
"<presence to='{name}@rooms.test/Alice'><x xmlns='http://jabber.org/protocol/muc'/></presence>"
|
|
),
|
|
);
|
|
send(
|
|
muc,
|
|
&format!(
|
|
"<iq type='set' to='{name}@rooms.test'><query xmlns='http://jabber.org/protocol/muc#owner'><x xmlns='jabber:x:data' type='submit'/></query></iq>"
|
|
),
|
|
);
|
|
}
|
|
|
|
fn chat(muc: &mut Muc) -> ModuleResult {
|
|
send(
|
|
muc,
|
|
"<message to='r@rooms.test' type='groupchat'><body>Fresh</body></message>",
|
|
)
|
|
}
|
|
|
|
fn mark(path: &Path) -> SystemTime {
|
|
let marker = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000);
|
|
fs::File::options()
|
|
.write(true)
|
|
.open(path)
|
|
.unwrap()
|
|
.set_times(fs::FileTimes::new().set_modified(marker))
|
|
.unwrap();
|
|
fs::metadata(path).unwrap().modified().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn groupchat_skips_native_rewrites_and_archive_retention_scan() {
|
|
let temp = tempfile::tempdir().unwrap();
|
|
let mut muc = Muc::new(temp.path().into(), "rooms.test".into());
|
|
muc.set_defaults(serde_json::json!({"persistent":true,"archiving":true}));
|
|
muc.set_archive_retention(Some(60.0));
|
|
room(&mut muc, "r");
|
|
room(&mut muc, "other");
|
|
|
|
let config = storage::path(temp.path(), "rooms.test", Some("r"), "config");
|
|
let other = storage::path(temp.path(), "rooms.test", Some("other"), "config");
|
|
let index = storage::path(temp.path(), "rooms.test", None, "persistent");
|
|
let journal = storage::path(temp.path(), "rooms.test", None, "rust_muc_checkpoint");
|
|
let config_time = mark(&config);
|
|
let other_time = mark(&other);
|
|
let index_time = mark(&index);
|
|
let journal_time = mark(&journal);
|
|
|
|
// An unreadable historic record makes any accidental full archive scan fail.
|
|
// Appending a fresh record needs only the existing file length, never its XML.
|
|
let log = storage::path(temp.path(), "rooms.test", Some("r"), "muc_log").with_extension("list");
|
|
fs::create_dir_all(log.parent().unwrap()).unwrap();
|
|
let historic = b"item(this is intentionally invalid historic archive data);\n";
|
|
fs::write(&log, historic).unwrap();
|
|
let delivered = chat(&mut muc);
|
|
assert_eq!(delivered.deliveries.len(), 1);
|
|
let contents = fs::read(&log).unwrap();
|
|
assert!(contents.starts_with(historic));
|
|
assert!(contents.len() > historic.len());
|
|
assert!(archive::load_store(temp.path(), "rooms.test", Some("r"), "muc_log").is_err());
|
|
for (path, before) in [
|
|
(&config, config_time),
|
|
(&other, other_time),
|
|
(&index, index_time),
|
|
] {
|
|
assert_eq!(fs::metadata(path).unwrap().modified().unwrap(), before);
|
|
}
|
|
assert_ne!(
|
|
fs::metadata(&journal).unwrap().modified().unwrap(),
|
|
journal_time
|
|
);
|
|
assert!(fs::read_to_string(journal).unwrap().contains("Fresh"));
|
|
}
|
|
|
|
#[test]
|
|
fn native_exports_repair_missing_files_and_export_changed_config() {
|
|
let temp = tempfile::tempdir().unwrap();
|
|
let mut muc = Muc::new(temp.path().into(), "rooms.test".into());
|
|
muc.set_defaults(serde_json::json!({"persistent":true}));
|
|
room(&mut muc, "r");
|
|
room(&mut muc, "other");
|
|
let config = storage::path(temp.path(), "rooms.test", Some("r"), "config");
|
|
let other = storage::path(temp.path(), "rooms.test", Some("other"), "config");
|
|
let index = storage::path(temp.path(), "rooms.test", None, "persistent");
|
|
let original = fs::read(&config).unwrap();
|
|
fs::remove_file(&config).unwrap();
|
|
fs::remove_file(&index).unwrap();
|
|
assert_eq!(chat(&mut muc).deliveries.len(), 1);
|
|
assert_eq!(fs::read(&config).unwrap(), original);
|
|
let recovered = storage::load(temp.path(), "rooms.test", None, "persistent")
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
recovered.get("r@rooms.test"),
|
|
Some(&storage::LuaValue::Bool(true))
|
|
);
|
|
assert_eq!(
|
|
recovered.get("other@rooms.test"),
|
|
Some(&storage::LuaValue::Bool(true))
|
|
);
|
|
let other_time = mark(&other);
|
|
let index_time = mark(&index);
|
|
let result = send(
|
|
&mut muc,
|
|
"<iq type='set' to='r@rooms.test'><query xmlns='http://jabber.org/protocol/muc#owner'><x xmlns='jabber:x:data' type='submit'><field var='muc#roomconfig_roomname'><value>Updated</value></field></x></query></iq>",
|
|
);
|
|
assert!(result.replies[0].contains("result"));
|
|
assert!(fs::read_to_string(config).unwrap().contains("Updated"));
|
|
assert_eq!(fs::metadata(other).unwrap().modified().unwrap(), other_time);
|
|
assert_eq!(
|
|
fs::metadata(&index).unwrap().modified().unwrap(),
|
|
index_time
|
|
);
|
|
|
|
// Export failures are nonfatal after the authoritative room commit, and
|
|
// exports must be retried even if no configuration changed since failure.
|
|
fs::remove_file(&index).unwrap();
|
|
fs::create_dir(&index).unwrap();
|
|
assert_eq!(chat(&mut muc).deliveries.len(), 1);
|
|
fs::remove_dir(&index).unwrap();
|
|
assert_eq!(chat(&mut muc).deliveries.len(), 1);
|
|
let recovered = storage::load(temp.path(), "rooms.test", None, "persistent")
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
recovered.get("r@rooms.test"),
|
|
Some(&storage::LuaValue::Bool(true))
|
|
);
|
|
assert_eq!(
|
|
recovered.get("other@rooms.test"),
|
|
Some(&storage::LuaValue::Bool(true))
|
|
);
|
|
}
|