Larger JSON stores
Small settings files and larger user-owned indexes have different durability needs. For state that is expensive for the user to recreate, use a staged write that validates the current file before promoting it to a backup.
- Use a same-directory temporary file and serialize writers for the destination.
- Flush and
fsyncthe temporary file when data loss would be expensive. - Promote the active file to
.bakonly after it parses successfully. - Define a loader that validates any selected backup before using it; the writer below supplies no loader.
- Own temporary cleanup and traceback-bearing diagnostics explicitly; cleanup can itself fail.
import json
import os
import threading
from debug_utils import LOG_CURRENT_EXCEPTION
def write_json_with_backup(path, data):
directory = os.path.dirname(path)
if directory and not os.path.isdir(directory):
os.makedirs(directory)
tmp_path = '%s.%s.%s.tmp' % (
path, os.getpid(), threading.current_thread().ident)
backup_path = path + '.bak'
try:
with open(tmp_path, 'w') as stream:
json.dump(data, stream, indent=2, sort_keys=True)
stream.flush()
os.fsync(stream.fileno())
if os.path.isfile(path):
current_is_valid = False
with open(path, 'r') as stream:
try:
json.load(stream)
except ValueError:
LOG_CURRENT_EXCEPTION()
else:
current_is_valid = True
if current_is_valid:
if os.path.isfile(backup_path):
os.remove(backup_path)
os.rename(path, backup_path)
else:
os.remove(path)
try:
os.rename(tmp_path, path)
except Exception:
if not os.path.isfile(path) and os.path.isfile(backup_path):
os.rename(backup_path, path)
raise
finally:
if os.path.isfile(tmp_path):
try:
os.remove(tmp_path)
except Exception:
LOG_CURRENT_EXCEPTION()
Directory setup and PID/thread-based name construction happen before the outer cleanup block. Failures there propagate without that cleanup. The suffix is neither a lock nor a write-operation token: successive or reentrant calls on one thread reuse it, and different temporary names still share the active/backup pair. Serialize that entire policy, including readers and external edits when their consistency matters.
The text-mode temporary open precedes chunked json.dump, so serialization or write
failures can leave truncated or partial staged data. Ordinary JSON defaults still allow non-finite
numbers and escape non-ASCII text. Flush, fsync and close precede inspection of the old file.
There is no immutable input snapshot, new-file readback, schema validation or size/latency bound.
isfile tests regular-file status; false can also mean a failed stat, not absence.
Only a true active-file probe admits validation. Opening is outside the parse handler.
A ValueError during json.load is logged inside that handler and leaves
the flag false; if logging and close succeed, that branch removes the active file and retains
any older backup. The exception type alone does not prove malformed bytes. Other read/parse
failures propagate. Parse success sets the flag before close, but close failure still prevents
rotation. Any decoded JSON value passes, without a root-type, schema or finite-number check;
the decoded value is discarded.
A true flag removes an existing regular-file backup, then renames active to backup. Backup removal, rotation and invalid-active removal are outside the promotion handler. Their failures propagate through cleanup without attempting its restore branch; earlier side effects are not rolled back. Separate probes and mutations do not give a consistent view of concurrent changes.
Only the final temporary-to-active rename has the Exception restore handler. It
attempts restoration when active is not a regular file and backup is a regular file; an existing
directory or failed stat can satisfy the first condition. It does not parse the backup or prove
this attempt created it, so retained older data can be selected. Successful restore consumes the
backup name and still re-raises the promotion failure; failed restore can replace that failure.
Unmet conditions re-raise without repair. There is no promotion retry or post-replacement check.
The outer finally probes the temporary file outside its cleanup handler. A false
probe can leave staged data; an exceptional probe can replace a pending error. An admitted
removal Exception is logged and swallowed if logging returns: a pending write error
continues, while a successful write can return None despite failed cleanup. Logging
failure can replace either outcome; exceptions outside Exception are not caught.
The function supplies no explicit success receipt, memory rollback, loader or stale-temp recovery.
The caught parse and cleanup failures call traceback-bearing LOG_CURRENT_EXCEPTION
without requiring Error Reporter. This fragment has no module version; a shipped mod must supply
version attribution and report other unexpected propagated failures at its owning boundary.
Logging establishes no cleanup, rollback or persistence result.
The active name can be absent between rotation and promotion. Temporary fsync, individual renames and PID/thread names do not make the sequence atomic or provide directory synchronization or verified crash/power-loss durability. A process crash does not run the restore handler. The staging skeleton and JSON settings recipe have different replacement, loading and cleanup contracts; do not substitute their guarantees.
Earlier in-memory controls compared parse/rotation handling with a repository implementation, using recording substitutes for filesystem and logging operations. They did not execute this exact illustration or test real disk writes, threads, Windows rename semantics or client runtime. This comparison adds source/control-flow review, with no new fixture or runtime execution.