Zum Inhalt

Race Conditions (P-005)

Problem

Timing-Konflikte zwischen FileWatcher und AutoSave können zu Dateninkonsistenzen führen.

Szenario 1: AutoSave während External Change

Diagramm

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

Diagramm

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%

Siehe auch