fix(storage): S-06 — container advisory lock prevents parallel mount and concurrent writes

This commit is contained in:
2026-09-19 09:05:12 +02:00
parent 9634beb744
commit f9733dfc73
7 changed files with 280 additions and 3 deletions
+1
View File
@@ -170,6 +170,7 @@ fn test_carrier_protection_in_storage_and_sync() {
checksum: false,
exclude_patterns: Vec::new(),
quiet: true,
force: false,
};
let pull_res = sanctum::sync::run_sync(
&db,
+117
View File
@@ -786,3 +786,120 @@ fn test_s05_zero_byte_file_truncate_removes_all_chunks() {
let _ = fs::remove_dir_all(&temp_root);
}
#[test]
fn test_s06_advisory_lock_blocks_sync_and_force_overrides() {
let temp_root =
std::env::temp_dir().join(format!("sanctum_s06_test_{}", rand::random::<u64>()));
let container_path = temp_root.join("test.sanctum");
let source_dir = temp_root.join("source");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("test_lock.txt"), b"Locked content").unwrap();
let (db, dek) = create_test_container(&container_path);
// 1. Initial: Kein Lock aktiv
assert!(db.check_advisory_lock().unwrap().is_none());
// 2. Lock setzen (simuliert laufenden Mount)
db.acquire_advisory_lock(false)
.expect("Acquire advisory lock");
let active_lock = db.check_advisory_lock().unwrap();
assert!(active_lock.is_some());
let (pid, _host, _time) = active_lock.unwrap();
assert_eq!(pid, std::process::id());
let mut opts = SyncOptions::default();
opts.direction = SyncDirection::Push;
opts.quiet = true;
opts.force = false;
// 3. Sync ohne --force muss mit Lock-Fehler abbrechen
let sync_res = run_sync(
&db,
0,
&dek,
FORMAT_VERSION,
source_dir.to_str().unwrap(),
"/",
&opts,
);
assert!(
sync_res.is_err(),
"Sync ohne --force muss bei aktivem Advisory Lock abbrechen"
);
let err_msg = sync_res.unwrap_err().to_string();
assert!(
err_msg.contains("Container ist gesperrt"),
"Fehlermeldung muss Lock-Hinweis enthalten: {}",
err_msg
);
// 4. Sync mit --force muss trotz Lock erfolgreich durchlaufen
opts.force = true;
let sync_force_res = run_sync(
&db,
0,
&dek,
FORMAT_VERSION,
source_dir.to_str().unwrap(),
"/",
&opts,
);
assert!(
sync_force_res.is_ok(),
"Sync mit --force muss trotz aktivem Advisory Lock gelingen"
);
// 5. Lock freigeben
db.release_advisory_lock().expect("Release advisory lock");
assert!(db.check_advisory_lock().unwrap().is_none());
// 6. Sync ohne --force gelingt jetzt wieder
opts.force = false;
let sync_unlocked = run_sync(
&db,
0,
&dek,
FORMAT_VERSION,
source_dir.to_str().unwrap(),
"/",
&opts,
);
assert!(sync_unlocked.is_ok());
// 7. Stale Lock Test: Lock mit nicht mehr existierender PID wird automatisch bereinigt
{
let conn = rusqlite::Connection::open(&container_path).unwrap();
let current_host = std::env::var("COMPUTERNAME")
.or_else(|_| std::env::var("HOSTNAME"))
.unwrap_or_else(|_| "localhost".to_string());
conn.execute(
"UPDATE meta SET lock_pid = 99999999, lock_host = ?1, lock_time = 1000 WHERE slot_id = 0",
rusqlite::params![current_host],
)
.unwrap();
}
// check_advisory_lock erkennt den toten Prozess und räumt den Lock auf
assert!(
db.check_advisory_lock().unwrap().is_none(),
"Stale Lock eines toten Prozesses muss automatisch als None bewertet werden"
);
let sync_stale = run_sync(
&db,
0,
&dek,
FORMAT_VERSION,
source_dir.to_str().unwrap(),
"/",
&opts,
);
assert!(
sync_stale.is_ok(),
"Sync muss bei verwaistem Lock eines toten Prozesses ohne --force gelingen"
);
let _ = fs::remove_dir_all(&temp_root);
}