feat(cross-platform): add QUICKSTART.md, live crash test, and platform abstraction module
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::Bytes;
|
||||
use dav_server::{
|
||||
davpath::DavPath,
|
||||
fs::{DavFileSystem, OpenOptions},
|
||||
};
|
||||
use rand::RngCore;
|
||||
use sanctum::{
|
||||
crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, FORMAT_VERSION},
|
||||
storage::Database,
|
||||
verify::verify_container,
|
||||
vfs::SanctumFs,
|
||||
};
|
||||
|
||||
/// Live-Crash- und Stresstest: Simuliert harten Verbindungsabbruch und Power-Cut
|
||||
/// während intensiver paralleler Schreibvorgänge im VFS.
|
||||
#[tokio::test]
|
||||
async fn test_live_crash_and_recovery_stress() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let container_path: PathBuf = temp_dir.join(format!("sanctum_live_stress_{}.sanctum", std::process::id()));
|
||||
if container_path.exists() {
|
||||
let _ = std::fs::remove_file(&container_path);
|
||||
}
|
||||
|
||||
let password = "LiveStressPassword2026!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).expect("KEK derivation");
|
||||
let dek = generate_dek();
|
||||
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).expect("DEK wrapping");
|
||||
|
||||
// 1. Initialisierung des Containers
|
||||
{
|
||||
let db = Database::open(&container_path).expect("Open database");
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag)
|
||||
.expect("Init schema");
|
||||
db.checkpoint().expect("Initial Checkpoint");
|
||||
}
|
||||
|
||||
// 2. Parallele Schreiblast mit SanctumFs erzeugen
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
let db = Database::open(&container_path).expect("Open database for VFS");
|
||||
let fs = SanctumFs::new(db, dek.clone(), FORMAT_VERSION);
|
||||
|
||||
let mut handles = Vec::new();
|
||||
|
||||
// Spawn 4 parallele Schreiber
|
||||
for worker_id in 0..4 {
|
||||
let fs_clone = fs.clone();
|
||||
let stop_clone = stop_signal.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut file_idx = 0;
|
||||
while !stop_clone.load(Ordering::Relaxed) && file_idx < 10 {
|
||||
let file_path_str = format!("/worker_{}_file_{}.dat", worker_id, file_idx);
|
||||
let dav_path = DavPath::new(&file_path_str).unwrap();
|
||||
|
||||
let mut opts = OpenOptions::default();
|
||||
opts.write = true;
|
||||
opts.create = true;
|
||||
opts.truncate = true;
|
||||
|
||||
// Datei anlegen
|
||||
let mut file = match fs_clone.open(&dav_path, opts).await {
|
||||
Ok(f) => f,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
// Mehrere 256-KB Blöcke schreiben (über mehrere Chunks hinweg)
|
||||
let mut payload = vec![0u8; 256 * 1024];
|
||||
rand::thread_rng().fill_bytes(&mut payload);
|
||||
|
||||
for _ in 0..6 {
|
||||
if stop_clone.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let _ = file.write_bytes(Bytes::copy_from_slice(&payload)).await;
|
||||
}
|
||||
let _ = file.flush().await;
|
||||
file_idx += 1;
|
||||
}
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Lass die Worker 500ms unter Volllast schreiben
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
// 3. Simuliere abrupten Prozessabbruch (Hard Kill / Power Cut)
|
||||
// Wir brechen die Worker hart ab (Cancel) und verwerfen das FS-Handle ohne sauberen Unmount
|
||||
stop_signal.store(true, Ordering::SeqCst);
|
||||
for h in handles {
|
||||
h.abort(); // Simuliert Kill
|
||||
}
|
||||
|
||||
// FS ohne Checkpoint/Drop-Finalisierung freigeben
|
||||
drop(fs);
|
||||
|
||||
// 4. Recovery & Integritätsprüfung nach Crash
|
||||
// Das System muss die SQLite WAL-Datei automatisch erkennen und verarbeiten
|
||||
let verify_result = verify_container(&container_path, Some(&dek), false).expect("Verify post-crash");
|
||||
assert!(
|
||||
verify_result.is_healthy(),
|
||||
"Container muss nach Crash vollkommen konsistent sein! Fehler: {:?}",
|
||||
verify_result.errors
|
||||
);
|
||||
assert_eq!(verify_result.corrupted_chunks, 0, "Keine korrupten Chunks erlaubt");
|
||||
|
||||
// 5. Konsistentes Weiterarbeiten nach dem Absturz
|
||||
let db_recovered = Database::open(&container_path).expect("Open database after crash");
|
||||
let fs_recovered = SanctumFs::new(db_recovered, dek.clone(), FORMAT_VERSION);
|
||||
|
||||
// Neue Datei im wiederhergestellten Dateisystem anlegen und lesen
|
||||
let recovery_test_path = DavPath::new("/post_crash_verification.txt").unwrap();
|
||||
{
|
||||
let mut opts = OpenOptions::default();
|
||||
opts.write = true;
|
||||
opts.create = true;
|
||||
opts.truncate = true;
|
||||
let mut file = fs_recovered
|
||||
.open(&recovery_test_path, opts)
|
||||
.await
|
||||
.expect("Create post-crash file");
|
||||
file.write_bytes(Bytes::from_static(b"Sanctum Crash Consistency Verified!"))
|
||||
.await
|
||||
.expect("Write post crash file");
|
||||
file.flush().await.expect("Flush post crash file");
|
||||
}
|
||||
|
||||
// Datei wieder einlesen
|
||||
{
|
||||
let mut opts = OpenOptions::default();
|
||||
opts.read = true;
|
||||
let mut file = fs_recovered
|
||||
.open(&recovery_test_path, opts)
|
||||
.await
|
||||
.expect("Read post-crash file");
|
||||
let bytes = file.read_bytes(1024).await.expect("Read bytes");
|
||||
assert_eq!(&bytes[..], b"Sanctum Crash Consistency Verified!");
|
||||
}
|
||||
|
||||
// Sauberes Aufräumen der Testdatei
|
||||
drop(fs_recovered);
|
||||
let _ = std::fs::remove_file(&container_path);
|
||||
}
|
||||
Reference in New Issue
Block a user