Скрипт обновил JSON, а читатель получил половину файла
Рассмотрим домашний сервер: один скрипт собирает показания датчиков в status.json, другой читает этот файл для страницы состояния.
Обновление выглядит привычно:
with open("status.json", "w", encoding="utf-8") as f:
json.dump(data, f)
Режим w обнуляет существующий файл при открытии. Между этим моментом и завершением записи читатель может получить пустой или частично записанный JSON. Если пишущий процесс завершится с ошибкой, такая версия останется на диске.
Для небольшого снимка данных на локальной файловой системе Linux удобно сначала подготовить новый файл рядом с основным, закрыть его и затем заменить основной через os.replace. Пример рассчитан на Python 3.8 и новее:
import json
import os
import tempfile
from pathlib import Path
def save_snapshot(path, data):
target = Path(path)
fd, name = tempfile.mkstemp(
dir=target.parent,
prefix=f".{target.name}."
)
temp = Path(name)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, allow_nan=False)
os.replace(temp, target)
finally:
temp.unlink(missing_ok=True)
save_snapshot("status.json", {"temperature": 23.7})
Каталог должен существовать, а у пользователя должны быть права на создание и замену файлов в нём. В примере читатель и писатель работают от одного пользователя: временный файл создаётся с правами 0600. При замене права прежнего status.json автоматически не сохраняются.
allow_nan=False заставляет сериализацию завершиться ошибкой при значениях NaN и бесконечности. Они не входят в стандарт JSON, поэтому прежний корректный снимок в этом случае лучше сохранить.
Временный файл создаётся в том же каталоге, чтобы замена происходила в пределах одной файловой системы. На Linux такая замена атомарна: при открытии status.json читатель получает прежнюю или новую версию. Промежуточного состояния с половиной нового JSON по этому пути нет. При самом первом запуске файла ещё может не быть — этот случай читатель обрабатывает отдельно.
Если читатель открыл старую версию до замены, его файловый дескриптор продолжит ссылаться на неё. Следующее открытие по имени уже даст новый файл.
У приёма есть границы. Два писателя всё ещё могут перезаписать результаты друг друга: последняя замена победит. Для согласования изменений нужна блокировка или другой способ координации. Атомарность также не гарантирует сохранность последней версии после отключения питания: для этого отдельно разбираются синхронизация файла и каталога.
При исключении в Python блок finally удаляет временный файл. После SIGKILL или отключения питания он может остаться в каталоге: finally в таких случаях не выполняется.
Проверка простая: вызвать ошибку сериализации до os.replace и убедиться, что старый снимок читается, а временный файл удалён. Затем выполнить успешное обновление и проверить новый JSON.
Для файла состояния полезное свойство именно такое: пока новый снимок не готов, читателям доступен предыдущий.