fix: publish managed PDF bytes under the platform durability barrier (F_FULLFSYNC) - #138
Conversation
EF-031 / #134. PRKS has one durability convention, in `backend/fs_durability.py`: file contents are flushed with the strongest barrier the platform offers before a durable name can point at them, and the directory entry is flushed after the rename that created it. #129 established it and #133 added `fsync_open_file()` for a file this process still holds open. The two paths that publish *canonical* managed-PDF bytes were only half converted. Their directory sync already went through the module, but their content sync was still a bare `os.fsync(fp.fileno())`. On macOS that is not a durability barrier -- it returns once the write reaches the drive, without waiting for the drive's own cache -- so a save or upload could report success over bytes that never reached stable storage. Linearization, which flushes through `fsync_file_path`, was the only step applying the strong barrier, and it is optional (`PRKS_PDF_LINEARIZE=0`, no `qpdf`, a rewrite it declines): an optimization must never be what makes the first write durable. Both sites now call `fsync_open_file()`. The ordering and the fail-closed semantics are unchanged and now rest on the shared primitive: the temporary is durable before `os.replace()` publishes it, and a refused sync removes the temporary and propagates, leaving the previous canonical PDF in place; the exclusive create is durable before its name is returned, and a refused sync removes the partial file and raises `ManagedPdfStoreError`. No platform logic is duplicated here -- `F_FULLFSYNC` stays owned by `fs_durability`. The two halves of the convention stay distinct rather than converging on one state machine. Content durability is fatal. Directory durability is best-effort, because the operation does not exist everywhere and the rename has already happened by then, so `fsync_managed_pdf_parent()`'s boolean is now recorded in a metadata-only log line instead of being dropped. Tests: tests/test_managed_pdf_write_durability.py pins the call order (`fsync_open_file` strictly before `os.replace` / before success), the fail-closed behaviour and its cleanup, that `F_FULLFSYNC` is what the content sync reaches for when the platform has one, that the plain `os.fsync` fallback still runs where it does not, and -- statically -- that no bare `os.fsync` or platform branch comes back to this module. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLLQeu89khtoaZtom35buk
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review. 📝 WalkthroughWalkthroughManaged PDF replacement and upload paths now use shared durability primitives. Content-sync failures abort and clean up writes. Directory-sync refusals log warnings while retaining completed files. Tests cover ordering, cleanup, platform barriers, and primitive usage. ChangesManaged PDF durability
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix · Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to The change is intended to make managed PDF writes durable while failing closed on content-sync errors. No current production risk requiring resolution before merge is identified. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
PR Summary by QodoUse platform durability barrier for managed PDF publication
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
|
The module-level durability paragraph said contents are flushed "before a durable name can point at them". That describes `atomic_replace_managed_pdf_bytes()` exactly -- the temporary is synced before `os.replace()` publishes the canonical name -- but it cannot describe `store_new_managed_pdf_bytes()`, where the exclusive create is what reserves the name and so necessarily holds it before the sync runs. The guarantee there is about the report, not the name: the file counts as a stored PDF only once the store returns its name for a Work to reference, and the contents have passed the barrier by then. Spell both shapes out rather than generalizing to the weaker of the two, and make the `store_new_managed_pdf_bytes()` docstring, the comment beside its sync, and the regression module's docstring say the same thing. Prose only: no behaviour, ordering, error handling or test strategy changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLLQeu89khtoaZtom35buk
|
All 10 check runs are green on The one item still showing as failed is CodeRabbit's non-blocking Docstring Coverage pre-merge check (36.36% vs. an 80% default threshold). Not acting on it, for two reasons:
For scale, the two sibling suites this file was modelled on sit at 22.7% ( Generated by Claude Code |
Fixes #134 (EF-031).
What was wrong
PRKS has one durability convention, in
backend/fs_durability.py: file contents are flushed with the strongest barrier the platform offers before anything may treat them as stored, and the directory entry is flushed after the rename that created it. #129 established the convention and #133 addedfsync_open_file(fd)for a file this process still holds open.The two paths that publish canonical managed-PDF bytes were only half converted. Verified against current
master(9b2d8dc) before changing anything — they were the only two bare contentos.fsync()calls left inbackend/:atomic_replace_managed_pdf_bytes()— content sync beforeos.replace()store_new_managed_pdf_bytes()— content sync after the exclusive createBoth already routed their directory sync through
fsync_managed_pdf_parent()→fsync_directory(), but their content sync was stillos.fsync(fp.fileno()). On macOS that is not a durability barrier: it returns once the write reaches the drive, without waiting for the drive's own cache — exactly the window a power loss falls into. So an annotation materialization or an upload could report success over bytes that never reached stable storage.Linearization already flushes through
fsync_file_path(), which meant the optional step was the only one applying the strong barrier.maybe_linearize_pdf_in_place()is skipped wheneverPRKS_PDF_LINEARIZE=0,qpdfis missing, or it declines the rewrite, and even when it runs there is a crash window between the weak publish and its durable replace. An optimization must never be what makes the first write durable.What changed
Both sites now call
fsync_open_file()frombackend/fs_durability.py. No platform logic is duplicated here —F_FULLFSYNC,_sync_descriptorand thefcntlimport stay owned byfs_durability, and a new static test keeps it that way.Ordering and fail-closed semantics are unchanged in shape and now rest on the shared primitive. Where the barrier falls differs between the two shapes, and the docstrings say so rather than generalizing to the weaker of the two:
flush()→fsync_open_file()→os.replace()→ directory sync. A refused content sync removes the temporary and propagates, so the previous canonical PDF is untouched and nothing was published.flush()→fsync_open_file()→ directory sync → (optional) linearize → return the name. The file counts as a stored PDF only when that name comes back for a Work to reference. A refused content sync falls into the existingOSErrorhandler, which removes the partial file and raisesManagedPdfStoreError("write_failed"), so no name is handed back.Neither shape lets a caller learn of a managed PDF whose bytes have not passed the barrier.
Containment is untouched:
safe_pdf_path_under_dir()at runtime plus the CodeQL-documentednormpath(join(base, basename))+startswith(base)rebuild before every filesystem sink.File contents vs. directory entries
The two halves of the convention stay distinct rather than converging on one state machine, per
fs_durability's own contract:fsync_open_file()raises, and the caller abandons the write rather than presenting it as durable.fsync_managed_pdf_parent()keeps returning a boolean rather than raising.Reviewing that boolean (as #134 suggests) — it was previously dropped at both call sites. It is now recorded in a metadata-only log line (
pdf_replace_dir_sync_failed/pdf_upload_dir_sync_failed) so the weaker guarantee is surfaced, without changing return types, control flow, or inventing a restore-style state machine. The bytes are durable either way; only the entry is weaker.Callers
Checked every caller of both helpers. None assumed the old bare-
os.fsyncbehaviour:replace_managed_work_pdf()— a failingatomic_replace_managed_pdf_bytes()raises beforeos.replace(), so no COW exclusive file exists yet andcow_exclusive_unreferencedis correctly stillFalse.backend/server.pyPDF upload adapter — mapsManagedPdfStoreErroronto a response; unchanged.backend/backup_restore.pyonly referencesatomic_replace_managed_pdf_bytesin prose, as the containment pattern it mirrors.Tests
New:
tests/test_managed_pdf_write_durability.py(12 tests), call-order assertions throughout, no timing.atomic_replace_managed_pdf_bytes()syncs throughfsync_open_file()strictly beforeos.replace(), and the directory after — with the synced descriptor already carrying the whole body.store_new_managed_pdf_bytes()syncs before the directory sync, before linearization, and before returning a name.fsync_open_file()leaves the previous canonical PDF intact with no temp leftovers; the upload path raisesManagedPdfStoreError("write_failed"), removes the partial file, and never linearizes._FULLFSYNCforced on (the patterntests/test_pdf_linearize_durability.pyalready uses), both paths issueF_FULLFSYNCon the descriptor that becomes canonical, and plainos.fsyncis never called._FULLFSYNCoff, the plainos.fsyncfallback still flushes what it published.os.fsync(,F_FULLFSYNC,_sync_descriptororimport fcntlinwork_pdf_replace.py.Falsifiability checked: 11 of the 12 fail against the pre-change implementation.
Run:
python -m unittest tests.test_managed_pdf_write_durability tests.test_pdf_linearize_durability tests.test_restore_durability— 74 tests, OKpython run_tests.py(full unit/API/structural/Node suite) — 2308 tests, OKruff checkandpyrighton both touched files — cleanNo E2E was run: this is a filesystem-durability change with no UI surface.
Scope
Two functions in
backend/services/work_pdf_replace.pyplus one new test file, and a prose-only follow-up commit correcting the module's durability wording. No storage-layout, restore (#110), or linearization (#112) behaviour changes.🤖 Generated with Claude Code
https://claude.ai/code/session_01QLLQeu89khtoaZtom35buk
Summary by CodeRabbit