Race Conditions (P-005)¶
Problem¶
Timing-Konflikte zwischen FileWatcher und AutoSave können zu Dateninkonsistenzen führen.
Szenario 1: AutoSave während External Change¶
Timing:
t=0ms: User ändert F-0018 in App
t=10ms: AutoSave timer start (2000ms)
t=500ms: Claude schreibt Backlog.md (ändert F-0032)
t=1000ms: FileWatcher erkennt (nach 500ms debounce)
t=1001ms: App lädt external version
t=2010ms: AutoSave triggert → überschreibt!
Impact: Claude's Änderungen gehen verloren, User merkt es nicht.
Szenario 2: Doppeltes AutoSave¶
Erwartet: ✅ Funktioniert korrekt (Timer wird gecancelt)
Szenario 3: FileWatcher während AutoSave Write¶
t=0ms: AutoSave beginnt Write von Backlog.md
t=50ms: File-System-Event (modified)
t=100ms: Write fertig
t=500ms: FileWatcher debounce triggert
t=501ms: Hash-Check: Matches → Ignore (korrekt)
Erwartet: ✅ Funktioniert (Self-Write wird erkannt via Hash)
Szenario 4: External Change während AutoSave Write¶
Kritischster Fall:
t=0ms: AutoSave beginnt Write
t=10ms: Claude schreibt Backlog.md (parallel!)
t=50ms: AutoSave Write fertig
t=100ms: Hash gespeichert (von AutoSave Version)
t=550ms: FileWatcher debounce
t=551ms: Hash-Check: Mismatch → External Change erkannt
t=552ms: App lädt Claude's Version
Problem: Window von t=0 bis t=50 - beide schreiben gleichzeitig!
Wahrscheinlichkeit: Sehr gering (erfordert exaktes Timing), aber möglich.
Impact: Datei-Korruption möglich (gemischter Inhalt von beiden Writes).
Ursachen-Analyse¶
1. Unkoordinierte Timer¶
AutoSave: 2000ms debounce FileWatcher: 500ms debounce
Problem: Keine Synchronisation zwischen beiden.
2. Hash nach Write¶
Aktuell:
write(&file, content)?; // t=0
let hash = calculate_hash(content); // t=+5ms
store_hash(hash)?; // t=+10ms
Problem: Window zwischen write und store_hash.
3. Kein Locking¶
Problem: Kein File-Lock während Write-Operationen.
OS-Level: Mehrere Prozesse können gleichzeitig schreiben (last-write-wins).
Impact-Matrix¶
| Szenario | Wahrscheinlichkeit | Schwere | Datenverlust? |
|---|---|---|---|
| AutoSave überschreibt External | Mittel | Hoch | Ja |
| Doppeltes AutoSave | Hoch | Niedrig | Nein |
| FileWatcher während Write | Hoch | Niedrig | Nein |
| Parallel Writes | Sehr niedrig | Kritisch | Ja |
Lösungskonzepte¶
1. AutoSave Cancel bei External Change¶
// In useAutoSave.ts
useEffect(() => {
const handleExternalChange = () => {
// Cancel pending AutoSave timer
if (saveTimerRef.current) {
clearTimeout(saveTimerRef.current);
console.log('⚠️ AutoSave cancelled (external change detected)');
}
};
listen('backlog-file-changed', handleExternalChange);
}, []);
Vorteil: Verhindert Überschreiben von External Changes.
2. Write-Lock¶
use std::fs::OpenOptions;
fn write_with_lock(path: &Path, content: &str) -> Result<()> {
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)?;
// OS-level lock (blocking)
file.lock_exclusive()?;
file.write_all(content.as_bytes())?;
file.flush()?;
file.unlock()?;
Ok(())
}
Vorteil: Verhindert parallele Writes von App und External Process.
Nachteil: Blocking - Claude Code muss warten (aber nur ~10ms).
3. Hash vor Write¶
// 1. Berechne hash VOR write
let hash = calculate_hash(content);
// 2. Write mit Lock
write_with_lock(&path, content)?;
// 3. Speichere hash (bereits berechnet)
store_hash(&conn, &project_id, &hash)?;
Vorteil: Kein Window zwischen write und hash-store.
Test-Strategie¶
Unit-Test: AutoSave Cancel¶
test('AutoSave cancels on external change', async () => {
const { triggerSave } = useAutoSave();
// Start AutoSave
triggerSave();
// Simulate external change after 1s
await sleep(1000);
emit('backlog-file-changed', {});
// Wait for AutoSave timeout
await sleep(2000);
// Verify: File was NOT written
expect(writeCount).toBe(0);
});
Integration-Test: Parallel Writes¶
#[test]
fn test_parallel_writes() {
let file_path = temp_file();
// Thread 1: App AutoSave
let handle1 = thread::spawn(|| {
write_backlog(&file_path, "App version")?;
});
// Thread 2: External Write (Claude)
let handle2 = thread::spawn(|| {
write_backlog(&file_path, "Claude version")?;
});
handle1.join().unwrap();
handle2.join().unwrap();
// Verify: File is valid (one version won, not corrupted)
let content = read_file(&file_path)?;
assert!(content == "App version" || content == "Claude version");
assert!(parser::parse(&content).is_ok());
}
Metriken¶
Vor Fix¶
- AutoSave überschreibt External: ~5% der External Changes
- Hash-Timing-Problem: ~1% der Writes
- Parallel-Write-Korruption: <0.1% (sehr selten)
Nach Fix (Ziel)¶
- AutoSave überschreibt External: 0%
- Hash-Timing-Problem: 0%
- Parallel-Write-Korruption: 0%