46 lines
1.8 KiB
Python
46 lines
1.8 KiB
Python
"""Test-only fault control points (concept §18, US07-04).
|
|
|
|
Crash safety can only be proven by crashing at the exact moment a transition has
|
|
been persisted but its consequence has not. That needs a barrier *inside* the
|
|
production code path — but not a production capability: there is no endpoint, no
|
|
service method, and no configuration file entry that can trigger one. The only
|
|
switch is an environment variable naming a single point, read at the moment it is
|
|
passed, and the only thing it does is kill the process. A deployment that never
|
|
sets it can never reach the barrier.
|
|
|
|
``os._exit`` is deliberate: it skips atexit handlers, buffered flushes, and
|
|
``finally`` blocks, which is what a real ``SIGKILL`` or power loss does. A clean
|
|
shutdown would prove nothing.
|
|
|
|
The points are the persisted transitions of the journalled stages:
|
|
|
|
rename moving | moved | database_updated | verified | complete
|
|
archive transferring | verified | removing | source_removed | complete
|
|
exif exif:written — keywords on disk, checkpoint not yet recorded
|
|
upload upload:accepted — uploader exited, outcome not yet persisted
|
|
jobs job:item_done — item committed, job outcome not yet written
|
|
|
|
Recovery for each is asserted in tests/integration/test_fault_matrix.py and
|
|
tests/e2e/test_crash_recovery.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
ENV_VAR = "PHOTO_PIPELINE_FAULT_AFTER"
|
|
|
|
EXIF_WRITTEN = "exif:written"
|
|
UPLOAD_ACCEPTED = "upload:accepted"
|
|
JOB_ITEM_DONE = "job:item_done"
|
|
|
|
|
|
def maybe_fault(point: str) -> None:
|
|
"""Die abruptly when ``PHOTO_PIPELINE_FAULT_AFTER`` names ``point``.
|
|
|
|
Shared by the rename, archive, restore, EXIF, upload, and job lanes, each
|
|
passing its own state names. Never set the variable outside tests.
|
|
"""
|
|
if os.environ.get(ENV_VAR) == point:
|
|
os._exit(9)
|