Сбой записи оставил половину JSON: сохраняем файл через временный и replace
Приложение хранит небольшой снимок состояния в JSON. При сохранении файл открывают с режимом w, который обрезает прежнее содержимое ещё до окончания новой записи. Если сериализация или запись завершается ошибкой, на диске остаётся пустой либо частичный документ. Следующий запуск уже не может прочитать даже предыдущий корректный снимок.
Для такого хранения полезно разделить подготовку и публикацию. Сначала создаём временный файл в каталоге назначения, полностью записываем новую версию, закрываем его и лишь затем заменяем целевой путь через os.replace. До замены старый файл остаётся доступен. Временный файл рядом нужен, чтобы операция не пересекала разные файловые системы.
Подготовка до публикации
Ниже сериализация выполняется до открытия временного файла. Если данные не поддерживаются JSON, старое содержимое вообще не затрагивается. Имя временного файла уникально для вызова. На Windows важно закрыть файл перед заменой; поэтому NamedTemporaryFile используется с delete=False и выходит из with до os.replace.
import json
import os
import tempfile
from pathlib import Path
def save(path: Path, value) -> None:
text = json.dumps(value, ensure_ascii=False)
temporary = None
try:
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", dir=path.parent, delete=False
) as stream:
temporary = Path(stream.name)
stream.write(text)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "state.json"
path.write_text('{"status": "old"}', encoding="utf-8")
save(path, {"status": "new"})
assert json.loads(path.read_text(encoding="utf-8")) == {"status": "new"}
print("new")
Ошибки записи и параллельные авторы
Запустите пример Python 3.12. Он напечатает new. Для проверки отказа передайте объект object(), который json.dumps не сериализует, и убедитесь, что прежний JSON сохранился. Для ошибки публикации отдельно подмените os.replace на функцию с OSError и проверьте уборку временного файла. Так тест различает подготовку содержимого и изменение опубликованного пути.
У этого протокола точная цель: не публиковать частично подготовленное содержимое. Он не предотвращает потерю параллельного обновления. Два автора могут прочитать один снимок, подготовить разные версии, и последняя замена победит. Если такой сценарий допустим, понадобится блокировка или проверка версии. Для сложного многопользовательского состояния файловый снимок быстро перестаёт быть удобным хранилищем.
Атомарная замена имени также отличается от гарантии пережить отключение питания. Здесь flush и fsync применены к временному файлу, но требования к устойчивости записи каталога и особенности файловой системы зависят от платформы. Если нужна именно долговечность после аварии, выберите поддерживаемый для вашей системы протокол и проверьте его отдельно.
Не используйте фиксированное имя вроде data.tmp для всех вызовов: параллельные операции начнут мешать друг другу ещё до публикации. После успешной замены временного пути уже нет, после неуспеха его удаляет finally. Такой порядок делает локальное JSON-хранилище понятным: сначала строим целую новую версию, затем меняем доступное читателям имя.
Следующий шаг — перенести этот приём из отдельного файла в поддерживаемое приложение. В курсе «Python для продвинутых: ООП, типизация и тестирование» структура пакетов, объектная модель, mypy и pytest соединяются в типизированном трекере задач с хранением данных и логированием. Можно отработать связь между интерфейсом, реализацией и регрессионным тестом, а затем применить её к своему проекту.
Посмотреть программу и практику курса.
Документация: официальный источник по теме.
Другие задачи по теме: Типы, тесты и поведение программы.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
