diff --git a/cron/jobs.py b/cron/jobs.py index 4d5d84ec9..3af8278b4 100644 --- a/cron/jobs.py +++ b/cron/jobs.py @@ -347,22 +347,26 @@ def load_jobs() -> List[Dict[str, Any]]: def save_jobs(jobs: List[Dict[str, Any]]): - """Save all jobs to storage.""" + """Save all jobs to storage. Thread-safe via file lock.""" ensure_dirs() - fd, tmp_path = tempfile.mkstemp(dir=str(JOBS_FILE.parent), suffix='.tmp', prefix='.jobs_') - try: - with os.fdopen(fd, 'w', encoding='utf-8') as f: - json.dump({"jobs": jobs, "updated_at": _hermes_now().isoformat()}, f, indent=2) - f.flush() - os.fsync(f.fileno()) - os.replace(tmp_path, JOBS_FILE) - _secure_file(JOBS_FILE) - except BaseException: + import fcntl + lock_path = JOBS_FILE.parent / ".jobs.lock" + with open(lock_path, "w") as lock_fd: + fcntl.flock(lock_fd, fcntl.LOCK_EX) + fd, tmp_path = tempfile.mkstemp(dir=str(JOBS_FILE.parent), suffix='.tmp', prefix='.jobs_') try: - os.unlink(tmp_path) - except OSError: - pass - raise + with os.fdopen(fd, 'w', encoding='utf-8') as f: + json.dump({"jobs": jobs, "updated_at": _hermes_now().isoformat()}, f, indent=2) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp_path, JOBS_FILE) + _secure_file(JOBS_FILE) + except BaseException: + try: + os.unlink(tmp_path) + except OSError: + pass + raise def create_job(