Repository navigation
feat(supply): separate source progress signals #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
a9e0ed4
feat(supply): separate source progress signals (#29)
stone16 2d65a42
test(supply): prove progress visibility barriers (#29)
stone16 a1ccdbf
fix(supply): preserve progress order across rebuilds (#29)
stone16 6b0b39e
fix(supply): make progress ordering irreversible (#29)
stone16 2eebf04
fix(supply): complete progress lineage contracts (#29)
stone16 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
98 changes: 98 additions & 0 deletions
98
.../decisions/0043-separate-file-acquisition-progress-from-publication-progress.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| --- | ||
| name: adr-0043-separate-file-acquisition-progress-from-publication-progress | ||
| version: "1.0.0" | ||
| description: > | ||
| Persist source-scoped accepted-change checkpoints separately from contiguous | ||
| Runtime-visibility publication watermarks. | ||
| --- | ||
|
|
||
| # 0043. Separate File acquisition progress from publication progress | ||
|
|
||
| - Status: accepted | ||
| - Date: 2026-07-23 | ||
| - Refines: ADR-0037, ADR-0039, ADR-0040, ADR-0041, ADR-0042 | ||
|
|
||
| ## Context | ||
|
|
||
| A durable File import request proves that a source change was accepted, but it | ||
| does not prove that compilation, indexing, or publication completed. Treating | ||
| the import job state as one generic checkpoint makes operators unable to tell a | ||
| recoverable publication pause from completed Runtime visibility. It also makes | ||
| an out-of-order later completion capable of hiding an earlier failed change if | ||
| the source reports only its greatest completed ordinal. | ||
|
|
||
| Issue #29 needs two source-level progress signals without turning either signal | ||
| into Runtime authorization or claiming the deferred provider `checkpoint` | ||
| operation from the standard ProviderPort. | ||
|
|
||
| ## Decision | ||
|
|
||
| Each accepted File import job and trusted File tombstone receives one immutable, | ||
| database-ordered `file_source_acquisition_checkpoint`, scoped by Organization | ||
| and Source. The import checkpoint is appended in the same transaction that | ||
| creates its acquisition/job; the tombstone checkpoint is appended in the same | ||
| transaction as tombstone visibility and cleanup intent. Opaque references are | ||
| deterministic SHA-256-derived tokens, while positive sequence numbers remain | ||
| database-owned ordering facts. Idempotent replay does not append another row. | ||
|
|
||
| Each unchanged result, active Revision event, and tombstone appends one distinct | ||
| immutable `file_source_publish_watermark` row at its existing visibility | ||
| transaction boundary. Prepared, indexed, claimed, interrupted, or failed state | ||
| cannot create that row. Exact foreign keys bind each completion to its accepted | ||
| checkpoint, durable job or cleanup intent, Resource, and Revision. | ||
|
|
||
| The read-only `ContextControl.read_file_source_progress` seam returns the latest | ||
| accepted checkpoint and the highest *contiguous* completed sequence. If change | ||
| 3 completes while change 2 is paused, completion 3 remains durably recorded but | ||
| the exposed watermark remains 1. When recovery completes change 2, the exposed | ||
| watermark becomes 3 without rewriting either append-only stream. Calls are | ||
| operation-bound and Organization-scoped through a SECURITY DEFINER function; | ||
| all application roles are denied direct table access. | ||
|
|
||
| Runtime receives no table privilege or dependency on this status. Current | ||
| Resource state, active Revision, policy, audience, and AuthorizationKernel | ||
| checks remain the only delivery authority. The File capability manifest also | ||
| continues to report the standard provider cursor/checkpoint operations as | ||
| unavailable: this issue activates operational progress for the manual File | ||
| carrier, not provider `readChanges` acknowledgement. | ||
|
|
||
| ## Rationale | ||
|
|
||
| Acceptance and visibility have different transaction boundaries and different | ||
| failure meanings, so one mutable cursor cannot truthfully represent both. Two | ||
| append-only streams retain the causal history needed to diagnose a pause, | ||
| while exposing only the contiguous publication prefix prevents an | ||
| out-of-order completion from overstating Runtime freshness. Database-owned | ||
| ordering, deterministic opaque references, and exact durable-lineage foreign | ||
| keys make each accepted ordering fact stable without promoting progress | ||
| metadata into delivery authority. Existing durable lineage is ordered once | ||
| during the initial upgrade; after any progress exists, downgrade is refused so | ||
| concurrent online lock order can never be reconstructed differently or accept | ||
| new changes through an older migration boundary. | ||
|
|
||
| Keeping the operator read seam in ContextControl also preserves the existing | ||
| ProviderPort contract: File progress can be observed now without pretending a | ||
| provider-native change cursor has been acknowledged or making Runtime depend | ||
| on an operational status surface. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - An accepted change remains observable through compilation, indexing, | ||
| interruption, failure, and recovery without falsely claiming visibility. | ||
| - First publication, replacement, unchanged classification, and tombstone each | ||
| advance publication progress only inside their committed visibility outcome. | ||
| - Concurrent or out-of-order completion cannot skip an earlier gap; recovery | ||
| closes the gap and deterministically catches the watermark up. | ||
| - Both streams are append-only, forced-RLS tenant tables with no source content. | ||
| Downgrade is allowed only while both streams are empty; after any accepted | ||
| change, a forward fix is required so sequence numbers and opaque references | ||
| cannot be renumbered from historical timestamps. | ||
| - Cross-source aggregation, UI, provider cursor formats, resync, and watermark- | ||
| based authorization remain inactive. | ||
|
|
||
| ## Revisit trigger | ||
|
|
||
| Revisit before provider-native `readChanges`/checkpoint activation, source | ||
| offboarding, resync, progress compaction, cross-source aggregation, or deletion | ||
| recovery. Any change must preserve separate signals, contiguous watermarks, | ||
| exact tenant lineage, replay idempotency, and Runtime independence. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| """Read-only File Source acquisition-checkpoint and publication-watermark contracts.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass, field | ||
| from datetime import datetime | ||
| from enum import StrEnum | ||
| from uuid import UUID | ||
|
|
||
| from engine.control.contracts import ( | ||
| SourceRef, | ||
| _require_bounded_text, | ||
| _require_token, | ||
| _require_utc, | ||
| ) | ||
|
|
||
| _MAX_BIGINT = 9_223_372_036_854_775_807 | ||
|
|
||
|
|
||
| class FileSourceChangeKind(StrEnum): | ||
| """Closed durable change carriers that participate in File progress.""" | ||
|
|
||
| FILE_IMPORT = "file_import" | ||
| FILE_TOMBSTONE = "file_tombstone" | ||
|
|
||
|
|
||
| class FileSourcePublishOutcome(StrEnum): | ||
| """Closed visibility outcomes that may advance a publish watermark.""" | ||
|
|
||
| PUBLISHED = "published" | ||
| REPLACED = "replaced" | ||
| UNCHANGED = "unchanged" | ||
| TOMBSTONED = "tombstoned" | ||
|
|
||
|
|
||
| def _require_sequence(name: str, value: object) -> int: | ||
| if type(value) is not int or not 1 <= value <= _MAX_BIGINT: | ||
| raise ValueError(f"{name} must fit a positive signed bigint") | ||
| return value | ||
|
|
||
|
|
||
| def _require_resource_ref(value: object) -> str: | ||
| return _require_bounded_text("File Source progress ResourceRef", value, 512) | ||
|
|
||
|
|
||
| def _require_progress_ref(name: str, value: object, prefix: str) -> str: | ||
| token = _require_token(name, value) | ||
| digest = token.removeprefix(prefix) | ||
| if not token.startswith(prefix) or len(digest) != 64 or any( | ||
| character not in "0123456789abcdef" for character in digest | ||
| ): | ||
| raise ValueError(f"{name} is not a recognized opaque reference") | ||
| return token | ||
|
|
||
|
|
||
| def _require_lineage( | ||
| *, | ||
| change_kind: FileSourceChangeKind, | ||
| acquisition_ref: object, | ||
| job_ref: object, | ||
| cleanup_intent_ref: object, | ||
| resource_ref: object, | ||
| revision_ref: object, | ||
| event_ref: object, | ||
| event_sequence: object, | ||
| allow_unresolved_import_resource: bool, | ||
| ) -> None: | ||
| if type(change_kind) is not FileSourceChangeKind: | ||
| raise TypeError("File Source progress change_kind is invalid") | ||
| if change_kind is FileSourceChangeKind.FILE_IMPORT: | ||
| if type(acquisition_ref) is not UUID or type(job_ref) is not UUID: | ||
| raise TypeError("File import progress requires acquisition and job lineage") | ||
| if any( | ||
| value is not None | ||
| for value in (cleanup_intent_ref, event_ref, event_sequence) | ||
| ): | ||
| raise ValueError("File import progress cannot carry tombstone lineage") | ||
| if allow_unresolved_import_resource and resource_ref is revision_ref is None: | ||
| return | ||
| else: | ||
| if type(cleanup_intent_ref) is not UUID: | ||
| raise TypeError("File tombstone progress requires cleanup lineage") | ||
| if acquisition_ref is not None or job_ref is not None: | ||
| raise ValueError("File tombstone progress cannot carry import lineage") | ||
| _require_token("File tombstone progress event_ref", event_ref) | ||
| _require_sequence("File tombstone progress event_sequence", event_sequence) | ||
| _require_resource_ref(resource_ref) | ||
| if type(revision_ref) is not UUID: | ||
| raise TypeError("File Source progress revision_ref must be UUID") | ||
|
|
||
|
|
||
| @dataclass(frozen=True, slots=True) | ||
| class FileSourceAcquisitionCheckpoint: | ||
| """Latest source change durably accepted, irrespective of visibility.""" | ||
|
|
||
| sequence: int | ||
| checkpoint_ref: str = field(repr=False) | ||
| change_kind: FileSourceChangeKind | ||
| acquisition_ref: UUID | None = field(repr=False) | ||
| job_ref: UUID | None = field(repr=False) | ||
| cleanup_intent_ref: UUID | None = field(repr=False) | ||
| resource_ref: str | None | ||
| revision_ref: UUID | None = field(repr=False) | ||
| event_ref: str | None = field(repr=False) | ||
| event_sequence: int | None | ||
| accepted_at: datetime | ||
|
|
||
| def __post_init__(self) -> None: | ||
| _require_sequence("File Source acquisition sequence", self.sequence) | ||
| _require_progress_ref( | ||
| "File Source acquisition checkpoint_ref", self.checkpoint_ref, "facp_" | ||
| ) | ||
| _require_lineage( | ||
| change_kind=self.change_kind, | ||
| acquisition_ref=self.acquisition_ref, | ||
| job_ref=self.job_ref, | ||
| cleanup_intent_ref=self.cleanup_intent_ref, | ||
| resource_ref=self.resource_ref, | ||
| revision_ref=self.revision_ref, | ||
| event_ref=self.event_ref, | ||
| event_sequence=self.event_sequence, | ||
| allow_unresolved_import_resource=True, | ||
| ) | ||
| _require_utc("File Source acquisition accepted_at", self.accepted_at) | ||
|
|
||
|
|
||
| @dataclass(frozen=True, slots=True) | ||
| class FileSourcePublishWatermark: | ||
| """Latest contiguous accepted change fully reflected in Runtime visibility.""" | ||
|
|
||
| sequence: int | ||
| watermark_ref: str = field(repr=False) | ||
| checkpoint_ref: str = field(repr=False) | ||
| change_kind: FileSourceChangeKind | ||
| outcome: FileSourcePublishOutcome | ||
| acquisition_ref: UUID | None = field(repr=False) | ||
| job_ref: UUID | None = field(repr=False) | ||
| cleanup_intent_ref: UUID | None = field(repr=False) | ||
| resource_ref: str | ||
| revision_ref: UUID = field(repr=False) | ||
| event_ref: str | None = field(repr=False) | ||
| event_sequence: int | None | ||
| published_at: datetime | ||
|
|
||
| def __post_init__(self) -> None: | ||
| _require_sequence("File Source publish sequence", self.sequence) | ||
| _require_progress_ref( | ||
| "File Source publish watermark_ref", self.watermark_ref, "fpwm_" | ||
| ) | ||
| _require_progress_ref( | ||
| "File Source publish checkpoint_ref", self.checkpoint_ref, "facp_" | ||
| ) | ||
| if type(self.outcome) is not FileSourcePublishOutcome: | ||
| raise TypeError("File Source publish outcome is invalid") | ||
| _require_lineage( | ||
| change_kind=self.change_kind, | ||
| acquisition_ref=self.acquisition_ref, | ||
| job_ref=self.job_ref, | ||
| cleanup_intent_ref=self.cleanup_intent_ref, | ||
| resource_ref=self.resource_ref, | ||
| revision_ref=self.revision_ref, | ||
| event_ref=self.event_ref, | ||
| event_sequence=self.event_sequence, | ||
| allow_unresolved_import_resource=False, | ||
| ) | ||
| if ( | ||
| self.change_kind is FileSourceChangeKind.FILE_TOMBSTONE | ||
| ) is not (self.outcome is FileSourcePublishOutcome.TOMBSTONED): | ||
| raise ValueError("File Source publish outcome does not match its change") | ||
| _require_utc("File Source publish published_at", self.published_at) | ||
|
|
||
|
|
||
| @dataclass(frozen=True, slots=True) | ||
| class FileSourceProgress: | ||
| """Organization/Source-scoped read model for the two progress signals.""" | ||
|
|
||
| organization_id: UUID = field(repr=False) | ||
| source_ref: SourceRef | ||
| acquisition_checkpoint: FileSourceAcquisitionCheckpoint | None | ||
| publish_watermark: FileSourcePublishWatermark | None | ||
|
|
||
| def __post_init__(self) -> None: | ||
| if type(self.organization_id) is not UUID: | ||
| raise TypeError("File Source progress organization_id must be UUID") | ||
| if type(self.source_ref) is not SourceRef: | ||
| raise TypeError("File Source progress source_ref must be SourceRef") | ||
| if self.acquisition_checkpoint is not None and type( | ||
| self.acquisition_checkpoint | ||
| ) is not FileSourceAcquisitionCheckpoint: | ||
| raise TypeError("File Source acquisition checkpoint is invalid") | ||
| if self.publish_watermark is not None and type( | ||
| self.publish_watermark | ||
| ) is not FileSourcePublishWatermark: | ||
| raise TypeError("File Source publish watermark is invalid") | ||
| if self.publish_watermark is not None and ( | ||
| self.acquisition_checkpoint is None | ||
| or self.publish_watermark.sequence | ||
| > self.acquisition_checkpoint.sequence | ||
| ): | ||
| raise ValueError("File Source publish watermark cannot exceed checkpoint") | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.