Files
rust-xmpp/tests/sm_journal.rs
T
wt 5b8ee4f4ad
Test, build and publish / All tests and Linux binary (push) Failing after 14s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped
Revert "Версия 0.8.3 удерживала глобальный Store.slots mutex на весь durable"
This reverts commit 821e1c3e2f.
2026-10-21 15:23:34 +07:00

323 lines
11 KiB
Rust

//! Redo recovery is independent of the best-effort readable JSON export.
use rust_xmpp::sessions::Hub;
use std::{fs, io::Write, path::Path, time::Duration};
use tokio::sync::mpsc;
fn committed(dir: &Path) -> (String, Vec<u8>) {
let hub = Hub::persistent(dir).unwrap();
let (tx, _rx) = mpsc::channel(8);
let session = hub.bind("a@x/r", "c", tx, Duration::from_secs(60)).unwrap();
let mut state = session.lock().unwrap();
state.enable(true).unwrap();
state.transmit("durable".into()).unwrap();
let token = state.resume_id.clone().unwrap();
let json = fs::read(dir.join(format!("{token}.json"))).unwrap();
drop(state);
drop(hub);
(token, json)
}
#[test]
fn redo_repairs_missing_or_torn_export_before_json_validation() {
for torn in [false, true] {
let dir = tempfile::tempdir().unwrap();
let (token, _) = committed(dir.path());
let json = dir.path().join(format!("{token}.json"));
if torn {
fs::write(&json, b"{torn").unwrap();
} else {
fs::remove_file(&json).unwrap();
}
let hub = Hub::persistent(dir.path()).unwrap();
assert_eq!(
hub.get("a@x/r")
.unwrap()
.lock()
.unwrap()
.pending
.front()
.map(String::as_str),
Some("durable")
);
let export: serde_json::Value = serde_json::from_slice(&fs::read(json).unwrap()).unwrap();
assert_eq!(export["pending"][0], "durable");
}
}
#[test]
fn partial_tail_is_discarded_and_subsequent_commits_survive_restart() {
let dir = tempfile::tempdir().unwrap();
let (token, _) = committed(dir.path());
let wal = dir.path().join(format!("{token}.wal"));
let good_len = fs::metadata(&wal).unwrap().len();
fs::OpenOptions::new()
.append(true)
.open(&wal)
.unwrap()
.write_all(&[42, 0, 0])
.unwrap();
let hub = Hub::persistent(dir.path()).unwrap();
assert_eq!(fs::metadata(&wal).unwrap().len(), good_len);
hub.get("a@x/r")
.unwrap()
.lock()
.unwrap()
.mark_handled()
.unwrap();
drop(hub);
let hub = Hub::persistent(dir.path()).unwrap();
assert_eq!(hub.get("a@x/r").unwrap().lock().unwrap().handled, 1);
}
#[test]
fn complete_corrupt_frame_fails_closed_even_with_valid_export() {
let dir = tempfile::tempdir().unwrap();
let (token, _) = committed(dir.path());
let wal = dir.path().join(format!("{token}.wal"));
let mut bytes = fs::read(&wal).unwrap();
*bytes.last_mut().unwrap() ^= 1;
fs::write(wal, bytes).unwrap();
assert!(Hub::persistent(dir.path()).is_err());
}
#[test]
fn durable_tombstone_overrides_resurrected_stale_json() {
let dir = tempfile::tempdir().unwrap();
let (token, old) = committed(dir.path());
let hub = Hub::persistent(dir.path()).unwrap();
hub.kick_owner("a@x");
drop(hub);
fs::write(dir.path().join(format!("{token}.json")), old).unwrap();
let hub = Hub::persistent(dir.path()).unwrap();
assert!(hub.get("a@x/r").is_none());
}
#[test]
fn legacy_json_migrates_and_baseline_survives_partial_first_append() {
let dir = tempfile::tempdir().unwrap();
let (token, _) = committed(dir.path());
let wal = dir.path().join(format!("{token}.wal"));
fs::remove_file(&wal).unwrap();
let hub = Hub::persistent(dir.path()).unwrap();
hub.get("a@x/r")
.unwrap()
.lock()
.unwrap()
.mark_handled()
.unwrap();
drop(hub);
let bytes = fs::read(&wal).unwrap();
let baseline_len = u32::from_le_bytes(bytes[8..12].try_into().unwrap()) as usize;
fs::write(&wal, &bytes[..8 + 40 + baseline_len + 12]).unwrap();
let hub = Hub::persistent(dir.path()).unwrap();
let session = hub.get("a@x/r").unwrap();
let state = session.lock().unwrap();
assert_eq!(state.handled, 0);
assert_eq!(state.pending.front().map(String::as_str), Some("durable"));
}
#[test]
fn rotation_preserves_latest_snapshot_and_bounds_redo_size() {
let dir = tempfile::tempdir().unwrap();
let hub = Hub::persistent(dir.path()).unwrap();
let (tx, _rx) = mpsc::channel(8);
let session = hub.bind("a@x/r", "c", tx, Duration::from_secs(60)).unwrap();
let token;
{
let mut state = session.lock().unwrap();
state.enable(true).unwrap();
token = state.resume_id.clone().unwrap();
state.transmit("x".repeat(1024 * 1024)).unwrap();
for _ in 0..35 {
state.mark_handled().unwrap();
}
}
let wal = dir.path().join(format!("{token}.wal"));
assert!(fs::metadata(wal).unwrap().len() < 18 * 1024 * 1024);
drop(hub);
let hub = Hub::persistent(dir.path()).unwrap();
let session = hub.get("a@x/r").unwrap();
let state = session.lock().unwrap();
assert_eq!(state.handled, 35);
assert_eq!(state.pending.front().unwrap().len(), 1024 * 1024);
}
#[test]
fn incomplete_baseline_or_magic_never_falls_back_to_json() {
for keep in [0, 4, 8, 12, 48] {
let dir = tempfile::tempdir().unwrap();
let (token, _) = committed(dir.path());
let wal = dir.path().join(format!("{token}.wal"));
let bytes = fs::read(&wal).unwrap();
fs::write(wal, &bytes[..keep]).unwrap();
assert!(
Hub::persistent(dir.path()).is_err(),
"truncated baseline at {keep}"
);
}
}
#[test]
#[cfg(unix)]
fn journal_symlink_is_rejected_without_following_target() {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().unwrap();
let (token, _) = committed(dir.path());
let wal = dir.path().join(format!("{token}.wal"));
let other = tempfile::NamedTempFile::new().unwrap();
fs::remove_file(&wal).unwrap();
symlink(other.path(), wal).unwrap();
assert!(Hub::persistent(dir.path()).is_err());
}
#[test]
fn export_failure_after_commit_advances_memory_and_fences_later_writes() {
let dir = tempfile::tempdir().unwrap();
let hub = Hub::persistent(dir.path()).unwrap();
let (tx, _rx) = mpsc::channel(8);
let session = hub.bind("a@x/r", "c", tx, Duration::from_secs(60)).unwrap();
session.lock().unwrap().enable(true).unwrap();
let token = session.lock().unwrap().resume_id.clone().unwrap();
let obstruction = dir.path().join(format!("{token}.tmp"));
fs::create_dir(&obstruction).unwrap();
{
let mut state = session.lock().unwrap();
state.mark_handled().unwrap();
assert_eq!(state.handled, 1);
assert!(state.mark_handled().is_err());
assert_eq!(state.handled, 1);
}
drop(hub);
fs::remove_dir(obstruction).unwrap();
let hub = Hub::persistent(dir.path()).unwrap();
assert_eq!(hub.get("a@x/r").unwrap().lock().unwrap().handled, 1);
}
#[test]
fn retired_journals_are_collected_in_bounded_batches() {
let dir = tempfile::tempdir().unwrap();
let hub = Hub::persistent(dir.path()).unwrap();
for index in 0..64 {
let (tx, _rx) = mpsc::channel(1);
let connection = format!("c{index}");
let session = hub
.bind("a@x/r", &connection, tx, Duration::from_secs(60))
.unwrap();
session.lock().unwrap().enable(true).unwrap();
hub.disconnect(&connection, true);
}
assert_eq!(
fs::read_dir(dir.path())
.unwrap()
.filter(|entry| {
entry
.as_ref()
.unwrap()
.path()
.extension()
.is_some_and(|ext| ext == "wal")
})
.count(),
0
);
drop(hub);
assert!(Hub::persistent(dir.path()).unwrap().get("a@x/r").is_none());
}
#[test]
fn offline_checkpoint_repairs_exports_removes_redo_and_allows_legacy_restart() {
let dir = tempfile::tempdir().unwrap();
let (token, _) = committed(dir.path());
let json = dir.path().join(format!("{token}.json"));
fs::write(&json, b"{torn").unwrap();
assert_eq!(
rust_xmpp::session_store::checkpoint_exports(dir.path()).unwrap(),
1
);
assert!(!dir.path().join(format!("{token}.wal")).exists());
let export: serde_json::Value = serde_json::from_slice(&fs::read(json).unwrap()).unwrap();
assert_eq!(export["pending"][0], "durable");
let hub = Hub::persistent(dir.path()).unwrap();
let session = hub.get("a@x/r").unwrap();
assert_eq!(
session.lock().unwrap().pending.front().map(String::as_str),
Some("durable")
);
session.lock().unwrap().mark_handled().unwrap();
assert!(dir.path().join(format!("{token}.wal")).exists());
}
#[test]
fn offline_checkpoint_refuses_live_store_and_preserves_redo() {
let dir = tempfile::tempdir().unwrap();
let hub = Hub::persistent(dir.path()).unwrap();
let (tx, _rx) = mpsc::channel(8);
let session = hub.bind("a@x/r", "c", tx, Duration::from_secs(60)).unwrap();
session.lock().unwrap().enable(true).unwrap();
let token = session.lock().unwrap().resume_id.clone().unwrap();
assert!(rust_xmpp::session_store::checkpoint_exports(dir.path()).is_err());
assert!(dir.path().join(format!("{token}.wal")).exists());
}
#[test]
fn offline_checkpoint_durably_applies_expiration_and_tombstones() {
let dir = tempfile::tempdir().unwrap();
let hub = Hub::persistent(dir.path()).unwrap();
let (tx, _rx) = mpsc::channel(8);
let session = hub.bind("a@x/r", "c", tx, Duration::from_secs(60)).unwrap();
let token;
{
let mut state = session.lock().unwrap();
state.enable(true).unwrap();
token = state.resume_id.clone().unwrap();
state.expires = Some(std::time::Instant::now());
state.checkpoint().unwrap();
}
drop(hub);
assert_eq!(
rust_xmpp::session_store::checkpoint_exports(dir.path()).unwrap(),
0
);
assert!(!dir.path().join(format!("{token}.json")).exists());
assert!(!dir.path().join(format!("{token}.wal")).exists());
assert!(Hub::persistent(dir.path()).unwrap().get("a@x/r").is_none());
}
#[test]
fn superseded_frames_still_require_valid_checksums_json_and_baseline_flags() {
use sha2::{Digest, Sha256};
for damage in ["checksum", "json", "baseline"] {
let dir = tempfile::tempdir().unwrap();
let (token, _) = committed(dir.path());
let wal = dir.path().join(format!("{token}.wal"));
let mut bytes = fs::read(&wal).unwrap();
// Damage the baseline, leaving both later committed snapshots intact.
let mut len = u32::from_le_bytes(bytes[8..12].try_into().unwrap());
let mut payload = 48..48 + len as usize;
match damage {
"checksum" => bytes[payload.end - 1] ^= 1,
"json" => bytes[payload.end - 1] = b'!',
"baseline" => {
let start = bytes[payload.clone()]
.windows(4)
.position(|part| part == b"true")
.unwrap()
+ payload.start;
// Preserve valid JSON, but make the first frame a continuation.
bytes.splice(start..start + 4, b"false".iter().copied());
len += 1;
payload.end += 1;
bytes[8..12].copy_from_slice(&len.to_le_bytes());
bytes[12..16].copy_from_slice(&(!len).to_le_bytes());
}
_ => unreachable!(),
}
if damage != "checksum" {
let checksum = Sha256::digest(&bytes[payload]);
bytes[16..48].copy_from_slice(&checksum);
}
fs::write(wal, bytes).unwrap();
assert!(Hub::persistent(dir.path()).is_err(), "{damage}");
}
}