mirror of
https://github.com/cft0808/edict.git
synced 2026-09-03 07:17:16 +08:00
Background dispatch threads, the periodic scheduler scan, and HTTP
handlers all followed a load_tasks() → modify → save_tasks() pattern
without any mutual exclusion. The atomic_json_write only protects the
write itself; it does NOT prevent a concurrent reader from loading a
stale snapshot and overwriting another thread's changes.
Scenario (before this fix):
1. HTTP handler calls load_tasks() — gets snapshot A
2. Dispatch thread calls load_tasks() — gets the same snapshot A
3. HTTP handler modifies task X, calls save_tasks() — writes A'
4. Dispatch thread modifies task Y, calls save_tasks() — writes A''
(based on A, not A'), silently losing the HTTP handler's changes
to task X
This is a classic TOCTOU (Time-of-Check-Time-of-Use) race. The project
already has atomic_json_update() in file_lock.py that holds an exclusive
lock for the entire read-modify-write cycle, but none of the task
mutation paths used it.
Fix:
- Add modify_tasks(modifier) wrapper around atomic_json_update +
refresh trigger
- Add modify_task(task_id, updater) convenience wrapper for single-task
mutations
- Convert _update_task_scheduler (called from dispatch daemon threads)
to use modify_task
- Convert handle_scheduler_scan (periodic background scanner) to use
modify_tasks, with dispatch side-effects deferred until after the
lock is released
- Convert handle_scheduler_retry and handle_scheduler_rollback to use
modify_task
HTTP handler paths (handle_task_action, handle_review_action, etc.)
still use load_tasks/save_tasks for now — they run in the main thread
and are lower risk — but can be migrated incrementally.
Add 17 regression tests covering: atomic API correctness, scheduler
update persistence, scan stall detection, concurrent write survival
(the actual race), source-level audit of critical paths, and backward
compatibility of load_tasks/save_tasks.