cadrumo.application.user_profile.bundle_export module¶
Single application authority for portable profile-bundle publication.
Both operator purposes – portable transfer and the subject-access request –
share this one service and one bundle schema; only their typed
ProfileBundleExportPurpose metadata differs. Publication is a
three-phase durable sequence so a crash in any window recovers honestly: the
service serializes to a restrictive temporary file, fsyncs it, records a durable
PREPARED operation-state journal OUTSIDE the target artifact (carrying the
payload digest), atomically replaces the target, fsyncs the parent directory,
transitions the journal to COMPLETED, and only then emits the completion
event before clearing the journal. A durably-published bundle is never
un-published; every crash window is reconciled without loss:
a
PREPAREDoperation whose destination does not hold the recorded digest never published – it is cleared as an orphan and no event is emitted;a
PREPAREDoperation whose destination matches the digest, or aCOMPLETEDoperation, published durably – reconciliation emits itsPROFILE_EXPORTEDevent (idempotently, from the fixedevent_occurred_at) so no durably-published bundle is ever left without its audit event.
Reconciliation is not an optional maintenance chore: export_profile_bundle()
runs it before every publication, so the next export an operator performs – to
any destination, for any profile – is what clears a previous crash’s orphan
journal and its leftover cleartext staged temp.
The sealed recovery archive is a separate surface and is not folded in here.
- class PreparedProfileExport(operation, staged_path, pointer, bundle, request)[source]¶
Bases:
objectIn-memory handle to one staged, journalled, not-yet-published export.
- Parameters:
operation (ProfileBundleExportOperation)
staged_path (Path)
pointer (ProfileBucketPointer)
bundle (UserProfilePortableExport)
request (ProfileBundleExportRequest)
-
operation:
ProfileBundleExportOperation¶
-
staged_path:
Path¶
-
pointer:
ProfileBucketPointer¶
-
bundle:
UserProfilePortableExport¶
-
request:
ProfileBundleExportRequest¶
- class ProfileBundleExportReconciliation(reconciled, failures)[source]¶
Bases:
objectOutcome of one reconciliation sweep across every export journal.
reconciledare the operations the sweep resolved and cleared;failuresare the ones it isolated and left journalled for a later attempt. An operation skipped because a live export holds its target lock is in neither: it is healthy in-flight work, not a failure.- Parameters:
reconciled (tuple[ProfileBundleExportOperation, ...])
failures (tuple[ProfileBundleExportReconcileFailure, ...])
-
reconciled:
tuple[ProfileBundleExportOperation,...]¶
-
failures:
tuple[ProfileBundleExportReconcileFailure,...]¶
- export_profile_bundle(request, *, profile_decode_context)[source]¶
Resolve, serialize, atomically publish, and record one profile export.
profile_decode_contextmust come from the caller’s pinned authority operation; no schema is discovered while exporting. Reconciles any crash-interrupted prior publication BEFORE taking this export’s target lock (see_reconcile_crash_orphans_before_publication()), then holds one exclusive lock on the resolved target for the whole publication so a concurrent export to the same file is excluded, and composesprepare_profile_export()andpublish_prepared_export().- Return type:
- Parameters:
request (ProfileBundleExportRequest)
profile_decode_context (ProfileDecodeContext)
- prepare_profile_export(request, *, journal=None, profile_decode_context)[source]¶
Record the PREPARED journal, THEN stage the bundle to a restrictive temp.
profile_decode_contextis required because preparing the export reads the encrypted profile record and must not rediscover its schema through a process-global or filesystem fallback.The journal is written BEFORE any cleartext reaches disk, naming the staged path it is about to write. That ordering is the whole crash contract: the reconciliation sweep is journal-driven, so cleartext staged without a journal entry pointing at it would be unreachable by every recovery surface. Staging first left exactly that window – serialization, category derivation, and a journal write that can wait on the repository lock all sat between the cleartext hitting disk and the record naming it – and a crash inside it stranded a readable profile bundle that no sweep could ever find (
sensitive-financial-data-secure-storage-only).A crash between the journal write and a completed staging is harmless: the reconciliation finds a
PREPAREDoperation whose destination does not hold the recorded digest, clears the staged path it names (whether that file exists, is half-written, or was never created), and emits no event.- Return type:
- Parameters:
request (ProfileBundleExportRequest)
journal (ProfileBundleExportJournalRepository | None)
profile_decode_context (ProfileDecodeContext)
- publish_prepared_export(prepared, *, journal=None, profile_decode_context)[source]¶
Atomically replace the target, mark COMPLETED, then emit the event.
Replaces the target with the staged temp and fsyncs the parent directory – the durability point – then transitions the operation journal to
COMPLETEDand emits thePROFILE_EXPORTEDcompletion event before clearing the journal. A durably-published bundle is NEVER un-published: if the completion event cannot be written, theCOMPLETEDjournal is left in place so a laterreconcile_prepared_exports()emits the pending event, and the failure is surfaced rather than rolled back. The event is derived from the operation’s fixedevent_occurred_at, so the live emission and any reconciliation emission collapse to one idempotent event.- Return type:
- Parameters:
prepared (PreparedProfileExport)
journal (ProfileBundleExportJournalRepository | None)
profile_decode_context (ProfileDecodeContext)
- reconcile_prepared_exports(*, journal=None, profile_decode_context)[source]¶
Reconcile crash-interrupted exports honestly in a fresh process.
export_profile_bundle()calls this before every publication (see_reconcile_crash_orphans_before_publication()), and the operator can invoke it directly, so recovery happens on the next export or on demand.Every operation is isolated. One journal that cannot be read, or one operation that cannot be finalised – a profile deleted after its crash, a destination on a volume that has gone away – is recorded in
failuresand the walk continues, so a single bad record can never starve every later-ordered operation of its recovery. A failed operation keeps its journal, so the next sweep retries it. Failures are reported rather than swallowed: a journal left behind may still describe cleartext bundle bytes on disk, which is precisely what an operator needs told.Each operation is reconciled only while holding the SAME per-destination lock a live
export_profile_bundle()holds across its whole publication. An operation whose target lock cannot be acquired without waiting is an in-flight export, not a crash orphan, and is skipped (a skip, not a failure) – so a reconcile running concurrently with a live same-target export can never unlink its live staged temp or delete its journal (which would fail the liveos.replace). Under the lock the operation is re-read and resolved by state:COMPLETED(replace already landed): the durably-published bundle is missing only its audit trail, so thePROFILE_EXPORTEDevent is emitted (idempotently, from the fixedevent_occurred_at) and the journal cleared.PREPAREDwhose destination content matches the recorded digest: the atomic replace landed but theCOMPLETEDtransition did not, so the bundle IS published – the event is emitted and the journal cleared, closing the replace-to-COMPLETEDcrash window.PREPAREDwith no matching published content: a genuine orphan – its staged temp is removed and its journal cleared, and no event is emitted for an artifact that was never durably published.
- Return type:
- Parameters:
journal (ProfileBundleExportJournalRepository | None)
profile_decode_context (ProfileDecodeContext)