Skip to content

Add ChangeFeed AllVersionsAndDeletes mode#4706

Open
yumnahussain wants to merge 26 commits into
mainfrom
yumnahussain/yumnahussain-cosmos-changefeed-avad
Open

Add ChangeFeed AllVersionsAndDeletes mode#4706
yumnahussain wants to merge 26 commits into
mainfrom
yumnahussain/yumnahussain-cosmos-changefeed-avad

Conversation

@yumnahussain

@yumnahussain yumnahussain commented Jul 7, 2026

Copy link
Copy Markdown
Member

Adds the AllVersionsAndDeletes change feed mode.

What it adds

  • DriverCosmosOperation::change_feed_all_versions_and_deletes factory and an all_versions_and_deletes header flag that emit A-IM: Full-Fidelity Feed (instead of Incremental Feed). Everything else on the wire is identical to LatestVersion. This mode always routes through Gateway 1.0 so the header is never dropped by a Gateway 2.0 downgrade.
  • SDKChangeFeedMode::AllVersionsAndDeletes. query_change_feed stays the single entry point and dispatches on the mode to pick the factory. In this mode the service returns every intermediate version plus deletes, so the ChangeFeedItem<T> envelope's previous() and metadata() (operation type, LSN, commit timestamp, previous-image LSN, TTL-expiry flag) are populated.
  • Start gatingBeginning and PointInTime are not supported for this mode; intermediate versions and deletes only exist within the container's retention window. The request is issued and the service returns 400 BadRequest. Use Now or resume from a continuation token.
  • Continuation token — the feed mode is now recorded in the token and validated on resume. Resuming a LatestVersion token as AllVersionsAndDeletes (or vice versa) is rejected with a 400. Only the marker is stored, so LatestVersion and query tokens keep their exact existing payload and legacy tokens are treated as LatestVersion.

Known limitations

  • Now is re-evaluated per range on first poll and is not converted to a concrete PointInTime; a range never polled before a checkpoint can miss changes between the original start and resume. Lossless per-range Now is a follow-up.

Tests

Iterator create/replace/delete envelope decoding; continuation-token mode encode/accept/reject unit tests; in-memory emulator end-to-end coverage (latest-version, envelope decoding, Beginning/PointInTime rejection); live emulator create/replace/delete, split/fan-out, and PointInTime rejection tests.

@github-actions github-actions Bot added the Cosmos The azure_cosmos crate label Jul 7, 2026
@yumnahussain
yumnahussain force-pushed the yumnahussain/yumnahussain-cosmos-changefeed-avad branch from c2445af to 93a13de Compare July 7, 2026 19:06
Base automatically changed from yumnahussain/cosmos-changefeed-pull-api to main July 7, 2026 21:17
yumnahussain added a commit that referenced this pull request Jul 7, 2026
Two nits from the deep PR review of #4706:

- cosmos_operation.rs: the ChangeFeedStartFrom::Now doc claimed
  AllVersionsAndDeletes *must* resolve Now to a concrete start before
  persisting, which contradicts the shipped, deliberately-documented
  per-range Now limitation in query_change_feed. Reworded to match.
- change_feed.rs: all_versions_and_deletes_rejects_beginning only
  asserted is_err(). It now asserts the client-side rejection carries a
  400 BadRequest, so a regression to a different status (or one that
  lets the request reach the emulator) fails the test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@yumnahussain
yumnahussain force-pushed the yumnahussain/yumnahussain-cosmos-changefeed-avad branch from 8a6b740 to 7ad4819 Compare July 8, 2026 22:19
yumnahussain and others added 6 commits July 8, 2026 15:52
The change feed page iterator now deserializes each item into the caller's
type directly instead of tolerantly unwrapping the wire-format `current`
field. Callers bind `T = ChangeFeedItem<YourDoc>` on `query_change_feed`
and read the post-change document via `current()`, so the whole wire
envelope is preserved and passed up to the customer rather than stripped by
the SDK.

`ChangeFeedItem<T>` models the full envelope: `current`, `previous`,
and `metadata` (with `ChangeFeedMetadata` / `ChangeFeedOperationType`).
All fields beyond `current` are optional, so `LatestVersion` items
(`{ current }` only) deserialize unchanged while the type is also ready for
full-fidelity reads that populate `previous` / `metadata`.

- Add `models::ChangeFeedItem<T>`, `ChangeFeedMetadata`,
  `ChangeFeedOperationType` with unit tests covering create/replace/delete
  envelopes and the metadata-absent LatestVersion shape.
- Make `ChangeFeedPageIterator` parse-only (drop the unwrap helpers).
- Update `query_change_feed` and iterator rustdoc/examples to the envelope contract.
- Migrate the emulator and split change-feed integration tests to `ChangeFeedItem<MockItem>`.
- CHANGELOG: note the envelope contract and the new types.

This is a breaking change to the unreleased change feed API from #4621.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The real Cosmos emulator's LatestVersion change feed returns a metadata
object carrying positional fields (lsn/crts) but no operationType. A
required operation_type made ChangeFeedItem<T> fail to deserialize those
responses (missing field \operationType\), which surfaced as six failing
emulator_tests::cosmos_change_feed::* tests on the windows_stable CI job.

Make operation_type Option<ChangeFeedOperationType> with #[serde(default)]
so every envelope field is optional and a single type tolerantly serves
both the LatestVersion and full-fidelity wire shapes. Accessors now return
Option; add a regression unit test for the partial-metadata shape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add an iterator-level test proving the parse-only iterator hands the
whole envelope to the caller, including a partial \metadata\ object
(lsn/crts, no operationType) that mirrors the real LatestVersion wire
shape. Previously only the envelope type itself was tested with metadata.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@yumnahussain
yumnahussain force-pushed the yumnahussain/yumnahussain-cosmos-changefeed-avad branch from 7ad4819 to d9e57bc Compare July 9, 2026 06:28
Stacks on the change-feed wire-envelope work (#4723) and reuses its
shared `ChangeFeedItem<T>` type. Adds only the full-fidelity delta:

- driver: `CosmosOperation::change_feed_all_versions_and_deletes`
  factory and a `full_fidelity_feed` header flag emitting
  `A-IM: Full-Fidelity Feed` in place of `Incremental Feed`.
- driver in-memory emulator: parse the `a-im` header and synthesize a
  `create` envelope for full-fidelity reads.
- SDK: `ChangeFeedMode::AllVersionsAndDeletes` and mode dispatch in
  `query_change_feed` selecting the factory; reject
  `ChangeFeedStartFrom::Beginning` (400 BadRequest) since intermediate
  versions and deletes only exist within the retention window.
- Tests: iterator create/replace/delete envelope decoding plus emulator
  end-to-end coverage (latest-version, full-fidelity, Beginning
  rejection). CHANGELOG entries for both crates.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@yumnahussain
yumnahussain force-pushed the yumnahussain/yumnahussain-cosmos-changefeed-avad branch from d9e57bc to 2032bc1 Compare July 9, 2026 06:30
@yumnahussain
yumnahussain changed the base branch from main to yumnahussain/cosmos-changefeed-envelope July 9, 2026 06:30
yumnahussain and others added 2 commits July 9, 2026 08:18
The vnext Cosmos emulator does not honor the change feed wire-format
header and returns bare documents instead of the `{ "current": ... }`
envelope. The parse-only iterator deserialized those directly into
`ChangeFeedItem<T>`, leaving `current` empty and panicking the emulator
change feed tests.

Give `ChangeFeedItem<T>` a tolerant `Deserialize`: an object carrying any
reserved envelope key (`current`/`previous`/`metadata`) is parsed as the
envelope; otherwise the whole document is mapped onto `current` so no
data is lost on non-enveloping backends. Real Cosmos still envelopes, so
`previous`/`metadata` are preserved there.

Add unit tests for the flat shape and a delete envelope (which carries no
`current`), and note the behavior in the changelog.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add the `ChangeFeedPolicy` model type and
`ContainerProperties::with_change_feed_policy()` so containers can
configure a full-fidelity retention window (`retentionDuration`, in
minutes), which the service requires to serve
`ChangeFeedMode::AllVersionsAndDeletes` reads.

Add integration coverage for AVAD:
- Emulator tests (emulator-only; the vnext Linux emulator does not
  support full fidelity): a create/replace/delete of one document
  surfaces as three envelopes with chained `previousImageLsn`, and
  creates fan out once each across multiple physical partitions.
- A live split test that captures a `Now` token, forces a real
  partition split, then verifies post-split creates and deletes resume
  exactly once with no baseline replay.
- Unit tests for `ChangeFeedPolicy` serialization and round-tripping.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@yumnahussain

Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

Copilot AI and others added 2 commits July 9, 2026 22:48
Co-authored-by: yumnahussain <161499581+yumnahussain@users.noreply.github.com>
Co-authored-by: yumnahussain <161499581+yumnahussain@users.noreply.github.com>

Copilot AI commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved. I merged yumnahussain/cosmos-changefeed-envelope into this branch and fixed the conflict in sdk/cosmos/azure_data_cosmos/CHANGELOG.md in commit 5e3c326 (with a follow-up changelog readability tweak in c69739e).

@yumnahussain

Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

…nvelope' into yumnahussain/yumnahussain-cosmos-changefeed-avad

# Conflicts:
#	sdk/cosmos/azure_data_cosmos/tests/in_memory_emulator_tests/mod.rs
#	sdk/cosmos/azure_data_cosmos_driver/src/in_memory_emulator/operations.rs

Co-authored-by: yumnahussain <161499581+yumnahussain@users.noreply.github.com>

Copilot AI commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the new merge conflicts and merged the updated stacked base branch into this PR in commit 4e30d25.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 8 comments.

Comment thread sdk/cosmos/azure_data_cosmos_driver/src/models/cosmos_operation.rs
Comment thread sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos_driver/src/models/cosmos_headers.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/tests/split_tests/cosmos_change_feed_split.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_change_feed.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/CHANGELOG.md Outdated
Comment thread sdk/cosmos/azure_data_cosmos/CHANGELOG.md Outdated
Comment thread sdk/cosmos/azure_data_cosmos/CHANGELOG.md
Copilot AI review requested due to automatic review settings July 17, 2026 16:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 13 comments.

Comment thread sdk/cosmos/azure_data_cosmos_driver/src/models/cosmos_operation.rs
Comment thread sdk/cosmos/azure_data_cosmos/src/options/change_feed.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/tests/split_tests/cosmos_change_feed_split.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/CHANGELOG.md Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos_driver/src/models/cosmos_headers.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/tests/split_tests/cosmos_change_feed_split.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_change_feed.rs Outdated
@NaluTripician
NaluTripician marked this pull request as draft July 17, 2026 16:32
@NaluTripician
NaluTripician marked this pull request as ready for review July 17, 2026 16:32
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
2 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Route full-fidelity (AllVersionsAndDeletes) change feed away from
Gateway 2.0, which does not forward the A-IM header and is excluded by
the GW2 spec; without this an AVAD read against a GW2-enabled account
silently downgrades to LatestVersion. Thread the full_fidelity_feed flag
into is_operation_supported_by_gateway_v2() at both routing call sites,
with eligibility + routing unit tests.

Also format the column-0 code left by the auto-fix commits so fmt --check
passes, restore the unrelated CHANGELOG entries that had been dropped,
correct the #4706 CHANGELOG and doc wording, and make the live split
test drain deadline/predicate-based so lagging post-split deletes are
not missed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 17, 2026 17:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

Comment thread sdk/cosmos/azure_data_cosmos/tests/split_tests/cosmos_change_feed_split.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/options/change_feed.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs Outdated
All versions and deletes mode can only start from "now" or resume from a
continuation token within the container's retention / continuous-backup
window. Per the Cosmos DB docs and the .NET/Java SDKs, reading from an
arbitrary point in time is not supported in this mode, yet the client
allowed PointInTime and the Beginning-rejection message even recommended
it.

Extend the client-side guard to reject ChangeFeedStartFrom::PointInTime
(alongside Beginning) for AllVersionsAndDeletes, correct the error and
CHANGELOG wording, and add an emulator test asserting the rejection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 17, 2026 20:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Comment thread sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs
Comment thread sdk/cosmos/azure_data_cosmos/tests/split_tests/cosmos_change_feed_split.rs Outdated
Reworks the AllVersionsAndDeletes change feed to defer unsupported
start-position rejection to the service and to make continuation
tokens mode-aware, per review feedback on PR #4706.

- Remove the client-side guard that rejected `Beginning`/`PointInTime`
  for AllVersionsAndDeletes; the service gates these. The in-memory
  emulator now stands in for the service: it parses `If-Modified-Since`
  and returns `400 Bad Request` for a full-fidelity read that starts
  from the beginning (no `If-None-Match`) or a point in time
  (`If-Modified-Since`). `Now` and continuation-token resume are
  allowed.
- Encode the feed mode in the continuation token (`cfm`), and reject a
  resume that switches modes with a clear `400`. Only the full-fidelity
  marker is stored, so LatestVersion and query tokens are unchanged and
  backward-compatible.
- Move the two AVAD start-rejection assertions onto the streamed first
  page (the request is now issued lazily) and add an in-memory
  `PointInTime` rejection test alongside the existing `Beginning` one.
- Apply reviewer doc suggestions and scrub "full fidelity" from the
  public docs in favor of "all versions and deletes"; drop the verbose
  Gateway 2.0 eligibility doc comment.
- Update both CHANGELOGs to describe service gating and mode-in-token.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 20, 2026 15:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (3)

sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs:470

  • This constructor can silently disable the mode it promises to enable: Duration::from_secs(59) becomes retentionDuration: 0, while 90 seconds is shortened to 60 seconds. Since the wire format only supports whole minutes, make this boundary fallible and reject zero or non-whole-minute durations instead of changing the caller's requested policy.
    pub fn all_versions_and_deletes(retention: Duration) -> Self {
        Self {
            retention_duration_minutes: (retention.as_secs() / 60).min(i32::MAX as u64) as i32,

sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs:897

  • This contradicts the PR description, which tells users to use an in-retention PointInTime for this mode. The implementation and new emulator tests reject that start instead. Please resolve which contract is intended and align the code, tests, changelog, API docs, and PR description so reviewers and users are not given opposite behavior.
    /// * Only [`ChangeFeedStartFrom::Now`] or resuming from a continuation token
    ///   is supported. [`ChangeFeedStartFrom::Beginning`] and
    ///   [`ChangeFeedStartFrom::PointInTime`] are **not** supported, because
    ///   intermediate versions and deletes are only retained within the
    ///   container's retention / continuous-backup window. The service gates
    ///   this and rejects such a request with `400 Bad Request`.

sdk/cosmos/azure_data_cosmos/tests/split_tests/cosmos_change_feed_split.rs:435

  • These lines are not rustfmt-formatted, so the repository's mandatory formatting check will fail. Run cargo fmt -p azure_data_cosmos over the complete change; the same formatter will also wrap the other newly added long expressions in this test.
got_creates.sort();
expected_creates.sort();

Comment thread sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs Outdated
Comment thread sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs
@yumnahussain yumnahussain changed the title Add ChangeFeed AllVersionsAndDeletes (full-fidelity) mode Add ChangeFeed AllVersionsAndDeletes mode Jul 20, 2026
LatestVersion reads can still carry partial change metadata (lsn/crts
without an operation type), which ChangeFeedItem accepts. The doc for
query_change_feed claimed only current() is populated in that mode,
which would lead callers to ignore metadata the service can return.
Reword to note metadata() may also be present while previous() is not.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 20, 2026 16:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (3)

sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_change_feed.rs:895

  • A single next() only polls one physical range: UnorderedMerge::next_page advances one child per page (unordered_merge.rs:24-26,78-104). Since this test deliberately creates 2+ ranges, writes routed to unpolled ranges establish their Now position only after the writes and can be omitted, making the fan-out test time out/fail. Poll once per current feed range before writing.
            // Prime the per-range `Now` positions before writing.
            let primed = iterator
                .next()
                .await
                .expect("change feed stream always yields a page")?;

sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs:446

  • The stated 1-hour minimum is inaccurate: the official Azure Cosmos DB .NET ChangeFeedPolicy documentation configures a 5-minute full-fidelity retention window, and the wire setting has minute granularity. This public documentation would incorrectly discourage supported shorter windows; avoid hard-coding an unverified range since validation is intentionally delegated to the service.
/// The retention window has minute granularity. On the service it must fall
/// within the supported range (currently 1 hour to 30 days) when this mode
/// is enabled; this type does not enforce that range, leaving the service as the
/// source of truth.

sdk/cosmos/azure_data_cosmos/tests/split_tests/cosmos_change_feed_split.rs:435

  • These added statements are not rustfmt-indented, so the repository's mandatory cargo fmt --check will fail. Run cargo fmt -p azure_data_cosmos before merging.
got_creates.sort();
expected_creates.sort();

Comment on lines +192 to +193
#[serde(rename = "cfm", default, skip_serializing_if = "Option::is_none")]
change_feed_full_fidelity: Option<bool>,
Comment on lines +212 to +216
if expected == TokenOperation::ChangeFeed {
// Legacy tokens without an encoded mode are treated as LatestVersion.
let token_full_fidelity = self.change_feed_full_fidelity.unwrap_or(false);
let operation_full_fidelity = operation.request_headers().full_fidelity_feed;
if token_full_fidelity != operation_full_fidelity {

@NaluTripician NaluTripician left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local deep review of the AllVersionsAndDeletes change feed mode. Overall this is a high-quality, unusually well-tested PR — the two dangerous wire paths (Gateway 2.0 A-IM downgrade protection, and continuation-token mode binding) are both handled correctly and tested. No blocking wire/correctness bugs.

The inline notes below are, in priority order: one High (lossy Now resume undercuts AVAD's core delete guarantee), three Medium (client-side fail-fast, sub-minute retention truncation, preview-gating), two Low, and a fmt nit. The top ask before "done" is the lossy Now resume.

/// this and rejects such a request with `400 Bad Request`.
/// * [`ChangeFeedStartFrom::Now`] is re-evaluated per range the first time a
/// range is polled, so a range that is never polled before a checkpoint
/// resumes from resume-time and can drop the intermediate versions and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Now re-evaluation is the one thing I'd want to resolve (or surface much louder) before this ships stable.

For LatestVersion, re-evaluating Now per range at first-poll is benign — the feed still converges to latest state under at-least-once delivery. For AllVersionsAndDeletes it's permanent data loss: intermediate versions and deletes only exist inside the retention window, so a range that's never polled before a checkpoint resumes from resume-time and silently drops every delete/version in that gap. It's reachable on the normal fan-out path (UnorderedMerge round-robins pages, so a full_container feed checkpointed after a few pages leaves some ranges holding an unresolved Now). Missing deletes is exactly the guarantee AVAD exists to provide, so an audit/cache-invalidation consumer can silently miss deletions with no error.

.NET/Java pin Now to a concrete per-range continuation (LSN/etag) at first poll so full-fidelity resume is lossless. Options, roughly in order of preference:

  1. For AVAD, resolve Now to a concrete per-range start before persisting the token.
  2. If that stays a follow-up, add a test that documents the current lossy behavior (a regression anchor for the future fix) and repeat this caveat directly on the AllVersionsAndDeletes enum variant, not just here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Fresh AllVersionsAndDeletes feeds now pin every range to a concrete starting ETag before the first page is returned (prime-on-first-drain in UnorderedMerge, gated by the planner on full_fidelity_feed && !is_resume). The first poll of each range is a start-from-Now 304 that still carries an ETag, so each range records its true start even if it is never served before a checkpoint. The snapshot then holds a per-range token for every range, so resume can't fall back to a resume-time Now and drop the versions/deletes in the gap. Now keeps its semantics (it isn't rewritten to a concrete PointInTime). Covered by driver priming unit tests plus an integration test (all_versions_and_deletes_pins_every_range_before_checkpoint) asserting both ranges are pinned after a single served page and both resume from their pinned ETags; a companion test pins the LatestVersion no-prime path.

// from the beginning of the container or from an arbitrary point
// in time is not supported in this mode; the service gates this
// and returns a `400 Bad Request`, so it is not re-validated
// client-side here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferring Beginning/PointInTime rejection entirely to the service means query_change_feed(...).await? succeeds and the 400 only surfaces on the first pages.next().await — often far from the call site that made the mistake. .NET/Java reject this combination client-side at build time with an immediate, offline, self-describing error.

Suggest failing fast here when mode == AllVersionsAndDeletes and start is Beginning/PointInTime with no continuation token, returning a client-side error ("use Now or resume from a continuation token"). It's cheap, matches the peer SDKs, and doesn't depend on the server (or the emulator stand-in) gating identically in every case. If server-authoritative gating is a deliberate choice, worth a one-line note on why.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

java and .net do this on client but it also means that once support for these is added it requires an sdk upgrade. Point in time for avad is something already being worked on by the service.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving start-position validation server-authoritative here rather than adding a client-side fail-fast. tvaron asked earlier in this review to drop the client-side guard, and the service/emulator already rejects unsupported starts (Beginning/PointInTime under AVAD) with 400. Duplicating that check on the client risks drifting from the service contract as retention/continuous-backup windows evolve. I've kept the why note in code next to the start handling so the choice is explicit.

/// `retention` is truncated.
pub fn all_versions_and_deletes(retention: Duration) -> Self {
Self {
retention_duration_minutes: (retention.as_secs() / 60).min(i32::MAX as u64) as i32,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sub-minute durations silently truncate to 0 here — and per this struct's own docs 0 disables all-versions-and-deletes. So ChangeFeedPolicy::all_versions_and_deletes(Duration::from_secs(30)) returns a policy that disables the very mode the method name promises to enable, with no error. A caller who confuses seconds/minutes (or passes from_secs(90) thinking "1.5 min") gets silent truncation, possibly below the service minimum or to a disabling value. The change_feed_policy_truncates_sub_minute_retention test currently enshrines the 90s→1min behavior.

Prefer rounding up to a 1-minute floor for any non-zero duration, or rejecting a duration that truncates to 0 (or below the documented service minimum), rather than silently producing a disabling policy.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. all_versions_and_deletes(Duration) now rounds any non-zero duration up to the 1-minute floor (.as_secs().div_ceil(60)) instead of truncating sub-minute values to 0, which previously disabled the mode. Test renamed to change_feed_policy_rounds_sub_minute_retention_up (30s->1min, 90s->2min, 0s->0).

/// A value of `0` (the default) disables all versions and deletes, leaving only
/// `LatestVersion` reads available.
#[serde(rename = "retentionDuration", default)]
pub retention_duration_minutes: i32,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: exposing retention_duration_minutes as a raw public i32 alongside the typed retention_duration() accessor is a bit of a footgun. A caller can bypass the constructor and assign a negative/out-of-range value directly; retention_duration() papers over negatives with .max(0), but serialization still sends the raw negative to the service. #[non_exhaustive] blocks external struct-literal construction (good), but the field stays directly assignable on an existing value. Consider a private field with a getter/with_* setter, or clamp/validate on serialize.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. retention_duration_minutes is now private with a retention_duration_minutes() getter, and serialization clamps negatives to 0 (serialize_with) so an invalid value can never reach the wire. The public constructor and retention_duration() are unchanged. Added change_feed_policy_clamps_negative_retention_on_serialize.

/// [`metadata`](crate::models::ChangeFeedMetadata). Reading from the
/// beginning is not supported for this mode because intermediate versions
/// and deletes are only retained within the container's retention window.
AllVersionsAndDeletes,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design question: should AllVersionsAndDeletes ship behind a preview feature (like preview_dtx in this same CHANGELOG) rather than going straight onto the stable surface? It's landing with a documented data-loss gap (the lossy Now resume) and a documented emulator gap (no delete/pre-image replay). Once this variant is stable, its lossy-Now semantics effectively become a compatibility contract. Preview-gating until lossless per-range Now lands would avoid users building on a mode that can silently miss deletes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping AllVersionsAndDeletes on the stable surface (no preview flag). The main concern behind gating was the lossy-Now resume gap, which is now closed by the per-range pinning fix above — a resumed feed no longer drops versions/deletes. The mode is also encoded in the continuation token, so a token can't be silently resumed in the wrong mode. Happy to revisit if you'd still prefer a preview gate.

);
}

#[test]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice thorough envelope test. One small gap: it asserts operation_type, lsn, previous_image_lsn, and time_to_live_expired, but not conflict_resolution_timestamp (crts) — even though crts is part of the AVAD metadata contract and the in-memory emulator synthesizes it. Covered indirectly by the existing create-envelope test, so minor, but asserting crts here would fully lock down the AVAD metadata shape.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added crts to the create entry of the envelope in deserializes_all_versions_and_deletes_page and assert metadata().conflict_resolution_timestamp() deserializes to the expected Duration.

.filter(|e| e.operation_type() == Some(ChangeFeedOperationType::Create))
.filter_map(|e| e.current().and_then(|c| c.id.clone()))
.collect();
got_creates.sort();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: these two lines (got_creates.sort(); / expected_creates.sort();) are at column 0 while the surrounding block is indented — cargo fmt --check should flag this. Worth confirming the fmt CI gate is actually green on this file.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Re-indented the two sort() lines to match the surrounding block. (Note: cargo fmt --check doesn't flag these, so it slipped past the gate — corrected manually.)

Make AllVersionsAndDeletes feeds lossless across a resume and tighten the
retention policy surface, per nalutripician's review of #4706.

- Driver: pin every range to a concrete start ETag on the first drain of a
  fresh full-fidelity feed (prime-on-first-drain in UnorderedMerge, gated by
  the planner on `full_fidelity_feed && !is_resume`). A range that is never
  served before a checkpoint now still resumes from its true start instead of
  a resume-time `Now`, so no versions/deletes in the gap are dropped.
  LatestVersion is unchanged. Adds driver priming unit tests plus two
  integration tests covering the AVAD pin and the LatestVersion no-prime path.
- ChangeFeedPolicy: round sub-minute retention up to a 1-minute floor instead
  of truncating to 0 (which silently disabled the mode); make
  `retention_duration_minutes` private with a getter and clamp negatives on
  serialize so an invalid value can never reach the wire.
- change_feed_iterator: assert `crts` (conflict resolution timestamp) in the
  AVAD envelope deserialization test.
- Docs: update the container_client `Now` caveat to reflect lossless resume;
  minor split-test re-indentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 20, 2026 23:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs:489

  • Duration::as_secs() truncates sub-second values, so a nonzero retention such as Duration::from_nanos(1) serializes as 0 and disables AllVersionsAndDeletes, contrary to this method's stated round-up contract. Include subsec_nanos() when deciding whether a partial minute remains.
            retention_duration_minutes: retention.as_secs().div_ceil(60).min(i32::MAX as u64)
                as i32,

Comment on lines +875 to +878
/// * [`ChangeFeedMode::LatestVersion`] (default) — the latest version of
/// each created or replaced item. `current()` holds the item and
/// `metadata()` may also be present (for example `lsn` and the commit
/// timestamp), while `previous()` is not populated.
Comment on lines +900 to +906
/// * When starting from [`ChangeFeedStartFrom::Now`], every range is pinned
/// to its concrete starting position before the first page is returned, so
/// a range that is never served before a checkpoint still resumes from its
/// true start rather than resume-time. No intermediate versions or deletes
/// are dropped across a resume. `Now` is deliberately **not** rewritten to
/// a concrete [`ChangeFeedStartFrom::PointInTime`], which would change its
/// semantics; each range instead captures its own server continuation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Cosmos The azure_cosmos crate

Projects

Status: Needs Attention

Development

Successfully merging this pull request may close these issues.

6 participants