CCExtractor / CCExtractor/taskwarrior-flutter

Non-atomic file writes causing data corruption and complete task loss during concurrent operations

Aperta
#638 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
bug
Lingua principale
Dart
Stelle
244
Fork
179
Merge medio
12h 42m
PR unite (30g)
2

Descrizione

### Describe your issue

The current implementation of task persistence in
lib/app/utils/home_path/impl/data.dart uses non-atomic, synchronous file operations without any locking mechanism, which introduces severe race conditions and data loss scenarios.

Specifically, the _mergeTasks() method:

Reads the entire file into memory
Clears the file (writeAsStringSync(''))
Rewrites tasks line-by-line using multiple append operations

This approach is unsafe under concurrent access or unexpected crashes.

Why this is critical
File is explicitly truncated before rewrite
Multiple independent write operations increase failure risk
No locking → race conditions between sync and local updates
Crash between truncate and write → permanent data loss

This impacts:

Sync operations
Local task creation
Background updates

### Steps to reproduce

Launch the app
Trigger a sync operation (fetch tasks from server)
At the same time, add a new task locally
Observe .task/all.data file behavior
Result:
File may become partially written or completely empty
Some or all tasks disappear permanently

### What was the expected result?

Task file updates should be atomic and crash-safe
Concurrent operations should not corrupt data
No data loss should occur under any circumstance
Code reference

Problematic implementation:

void _mergeTasks(List tasks) {
var lines = File('${home.path}/.task/all.data')
.readAsStringSync(); // NOT ATOMIC

var taskMap = { /* build map */ };

File('${home.path}/.task/all.data')
.writeAsStringSync(''); // TRUNCATE

for (var task in taskMap.values) {
File('${home.path}/.task/all.data').writeAsStringSync(
'$task\n',
mode: FileMode.append, // Multiple writes
);
}
}
**Root cause**
No file locking mechanism
Non-atomic write strategy
Multiple I/O operations instead of a single transaction
No crash recovery or backup strategy

**Key improvements:**
Use a mutex/lock (e.g., synchronized package)
Write to a temporary file first
Replace original file using atomic rename
Maintain a backup file for recovery
Replace multiple writes with single batch write
High-level approach:
await lock.synchronized(() async {
final tasks = _readAllTasks();

final updated = merge(tasks, newTasks);

await tempFile.writeAsString(allTasksInOneString);

if (mainFile.existsSync()) {
await mainFile.copy(backupFile.path);
}

await tempFile.rename(mainFile.path); // atomic replace
});
**Impact**
Prevents total data loss
Eliminates race conditions
Ensures crash-safe persistence
Improves reliability of sync + local updates

### Put here any screenshots or videos (optional)

_No response_

### How can we contact you (optional)

_No response_

### Would you like to work on this issue?

Yes

### By submitting this issue, I have confirmed that:

- [x] I have starred the repo ⭐ and watched 👀 it on GitHub and followed the contribution guidelines.

Guida per i contributori

Apri la guida per i contributori

Direzione di ricerca

Inizia leggendo lib/app/utils/home_path/impl/data.dart, in particolare _mergeTasks(), e riproduci lo scenario di sincronizzazione concorrente e aggiornamento delle attività locali descritto nell'issue. Traccia come viene letto e riscritto .task/all.data; il lavoro è completato quando le operazioni concorrenti non lasciano più dati delle attività parziali o vuoti dopo un arresto anomalo e gli aggiornamenti rimangono intatti dopo una sostituzione sicura in caso di arresto anomalo.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
dart
Ambito
mobile
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Tranquilla
Chiarezza
Abbastanza chiara
Idoneità per principianti
48/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.