Docs: Add Distributed Transactions REST wire contract - #6002
Open
Abhijeet Mohanty (jeet1995) wants to merge 10 commits into
Open
Docs: Add Distributed Transactions REST wire contract#6002Abhijeet Mohanty (jeet1995) wants to merge 10 commits into
Abhijeet Mohanty (jeet1995) wants to merge 10 commits into
Conversation
Add the authoritative wire-contract document describing the I/O interactions between CosmosClient and the Distributed Transactions Coordinator (DTC). Documents request/response headers, payloads, the read-transaction aggregate status model, and the 408/429/449 retry policies as implemented by CosmosClient. Intended as the source of truth for porting the DTX wire contract to azure-sdk-for-rust/sdk/cosmos. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Please follow the required format: "[Internal] Category: (Adds|Fixes|Refactors|Removes) Description"
Regex: (\[Internal\]|\[v4\] )?.{3}.+: (Adds|Fixes|Refactors|Removes) .{3}.+
The [Internal] prefix is a review hint indicating no customer-observable impact (test pipelines, CI, doc-only changes, pure internal refactors). It does not affect changelog generation — the changelog is maintained per-PR in ### Unreleased of changelog.md. See CONTRIBUTING.md for the full [Internal] definition and the preview-feature carve-out.
Examples:
- Diagnostics: Adds GetElapsedClientLatency to CosmosDiagnostics
- PartitionKey: Fixes null reference when using default(PartitionKey)
- [v4] Client Encryption: Refactors code to external project
- [Internal] Query: Adds code generator for CosmosNumbers for easy additions in the future
…regate section CosmosClient unwraps a 207 Multi-Status envelope into the first per-op result that is not 424 and is >= 400, inheriting its statusCode and subStatusCode. Add a §6 subsection with pseudocode, a worked example, a retry-classification note, and a Rust port note so the wire-contract doc fully captures the client-facing promotion behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Document that the outer committer loop retries any 449 whose body sets isRetriable:true (sub-status-agnostic), while the inner ClientRetryPolicy classifier only treats a 449 as coordinator-retriable when sub-status is 5352. Explain why this split is safe given the coordinator's 449 vocabulary, clarify that a read 449/Unknown is retried by the outer loop (not dropped), and add a Rust-port note against collapsing 449 handling into a single rule. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…veat Rewrite the intro paragraph so it presents both the write-transaction (200/452/449/408/429/400/403/500) and read-transaction (200/304/207/404/449/408) response-code contracts symmetrically, calling out 452 as write-only and 207 as read-only. Remove the activity-id caveat, which is not accurate. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…tion The wire-contract doc is meant to be an external, self-contained source of truth (e.g. for the Rust SDK port). The internal ADO PR 2154806 link is not resolvable outside the org and adds no technical value, so drop both references while preserving the surrounding technical content. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…edup Name a bodyless transport-level timeout as the sharpest case of the ordinary-write double-apply danger, and replace the vague 'coordinator deduplicates by token' with the concrete contract mechanism: the coordinator keys the transaction record on the idempotency token (create-first, read-on-conflict; empty token rejected), so a replay converges on the one transaction and returns its authoritative state. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…reference DTX operations run in gateway mode through the Compute Gateway, which handles address requests internally, so client-side address resolution does not apply. Also removes the Rust-port porting notes, keeping the document focused on documenting CosmosClient behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fabian Meiswinkel (FabianMeiswinkel)
previously approved these changes
Jul 6, 2026
Fabian Meiswinkel (FabianMeiswinkel)
left a comment
Member
There was a problem hiding this comment.
LGTM - Thanks!
Fabian Meiswinkel (FabianMeiswinkel)
added a commit
to Azure/azure-sdk-for-rust
that referenced
this pull request
Jul 7, 2026
## Summary This PR adds **preview Distributed Transactions (DTX)** support to the Rust Cosmos driver and SDK. Distributed Transactions extend Cosmos DB atomicity beyond `TransactionalBatch`'s single-container / single-partition-key boundary. With this change, Rust clients can prepare and execute multi-partition, multi-container transactions within a single Cosmos DB account through the service-side Distributed Transactions Coordinator (DTC). The implementation is intentionally gated behind a disabled-by-default `preview_dtx` feature because the DTX service feature is still in preview/test-account rollout and is **not production-ready**. ## Important References - .NET SDK source of truth: [Azure/azure-cosmos-dotnet-v3](https://github.com/Azure/azure-cosmos-dotnet-v3) - .NET DTX OpenSpec / proposal: [Azure/azure-cosmos-dotnet-v3#5781](Azure/azure-cosmos-dotnet-v3#5781) - DTX REST wire contract between SDK and service: [Azure/azure-cosmos-dotnet-v3#6002](Azure/azure-cosmos-dotnet-v3#6002) - .NET DTX API review: [Azure/azure-cosmos-dotnet-v3#5877](Azure/azure-cosmos-dotnet-v3#5877) - Response reordering / fail-closed handling: [Azure/azure-cosmos-dotnet-v3#5974](Azure/azure-cosmos-dotnet-v3#5974) - DTX 429 / RUBudgetExceeded retry handling: [Azure/azure-cosmos-dotnet-v3#5989](Azure/azure-cosmos-dotnet-v3#5989) - Patch filter predicate support: [Azure/azure-cosmos-dotnet-v3#5995](Azure/azure-cosmos-dotnet-v3#5995) - Malformed DTX session-token handling: [Azure/azure-cosmos-dotnet-v3#5958](Azure/azure-cosmos-dotnet-v3#5958) - Rust design spec with ADRs: `sdk/cosmos/azure_data_cosmos_driver/docs/DISTRIBUTED_TRANSACTIONS_SPEC.md` ## What Changed ### Driver (`azure_data_cosmos_driver`) Adds preview DTX driver support behind `preview_dtx`: - DTX wire models: - `DistributedTransactionType` - `DistributedTransactionOperationKind` - `DistributedTransactionTarget` - `DistributedTransactionOperation` - `DistributedTransactionRequest` - `DistributedTransactionResponse` - `DistributedTransactionOperationResult` - `DistributedTransactionResultBody` - Request serialization for the DTC wire contract: - `POST /operations/dtc` - `x-ms-cosmos-idempotency-token` - `x-ms-cosmos-operation-type` - `x-ms-cosmos-resource-type: DistributedTransactionBatch` - JSON `operations[]` payload - Response parsing: - reorder by operation `index` - fail closed on malformed successful coordinator responses - preserve coordinator `isRetriable` and `diagnosticString` - preserve raw coordinator headers and raw per-operation response payloads for diagnostics and parity testing - promote `207 MultiStatus` according to the DTX contract - Two-tier retry behavior: - outer loop for body-bearing coordinator responses using `isRetriable` - inner retry classification for bodyless coordinator/infrastructure failures - DTX-specific retry budgets matching the .NET implementation - bodyless `429` uses the shared throttle retry path; body-bearing DTX `429` is handled by the outer coordinator retry path - Session handling: - pre-send session-token resolution for DTX operations - per-partition-key-range token resolution through the driver PKRange cache - parent-range fallback for freshly split children - response-side per-operation session-token merge - strict malformed-token rejection under Session consistency - DTC sub-status constants: - `DTC_COORDINATOR_RACE_CONFLICT` - `DTC_LEDGER_FAILURE` - `DTC_ACCOUNT_CONFIG_FAILURE` - `DTC_DISPATCH_FAILURE` - `DTC_OPERATION_ROLLED_BACK` ### SDK (`azure_data_cosmos`) Adds preview public SDK transaction builders behind `preview_dtx`. The SDK API now follows the same broad shape as `TransactionalBatch`: callers prepare a data-only transaction document and pass it to the account client for execution. - `DistributedWriteTransaction::new()` - `DistributedReadTransaction::new()` - `CosmosClient::commit_distributed_write(...)` - `CosmosClient::execute_distributed_read(...)` - Builder operation methods target `&ContainerClient`; the SDK resolves the underlying driver `ContainerReference` internally. - Same-account enforcement runs when the transaction is executed by `CosmosClient`. - `DistributedWriteTransaction` - `create_item` - `replace_item` - `upsert_item` - `delete_item` - `patch_item` - `DistributedReadTransaction` - `read_item` - `DistributedTransactionOperationOptions` - per-operation `session_token` - per-operation ETag `precondition` - `DistributedTransactionPatchOperationOptions` - per-operation `session_token` - per-operation ETag `precondition` - patch `filter_predicate` - `DistributedTransactionResponse` - `status` - `is_success_status_code` - `is_completed_status_code` - `len` - `is_empty` - `operation_result` - `headers` - `diagnostic_string` - `idempotency_token` - `is_retriable` - `error_message` - `diagnostics` - `activity_id` - `request_charge` - `retry_after_ms` - `DistributedTransactionOperationResult` - `index` - `status_code` - `sub_status_code` - `is_success_status_code` - `is_completed_status_code` - `etag` - `session_token` - `partition_key_range_id` - `request_charge` - `resource<T>()` `DistributedWriteTransaction` and `DistributedReadTransaction` are intentionally data-only builders. They do not carry a client and do not execute themselves. Execution happens through `CosmosClient::commit_distributed_write(...)` or `CosmosClient::execute_distributed_read(...)`, which consumes the prepared transaction and validates that every operation targets the same Cosmos DB account as the executing client. ### In-Memory Emulator Support DTX is not available in the normal emulator or emulator vnext today, and live DTX testing requires special test accounts. To enable local iteration, this PR adds DTX support to the in-memory emulator: - `POST /operations/dtc` - write transaction prepare / commit / rollback behavior - read transaction snapshot result rewriting - multi-container DTX sanity coverage - DTX Patch support, including filter predicate evaluation - prepared-operation rollback surfaced as `453 / 5415` - read snapshot failure rewrite to `424 FailedDependency` - no-partial-commit checks - `304 NotModified` read transaction handling ## Public API Example ```rust use azure_data_cosmos::{ CosmosClient, DistributedReadTransaction, DistributedWriteTransaction, PartitionKey, }; use azure_data_cosmos::clients::{ ContainerClient, DistributedTransactionOperationOptions, DistributedTransactionPatchOperationOptions, }; use azure_data_cosmos::models::{PatchInstructions, PatchOperation}; use azure_data_cosmos::options::Precondition; #[derive(serde::Serialize, serde::Deserialize)] struct Account { id: String, pk: String, balance: i64, } #[derive(serde::Serialize)] struct AuditEvent { id: String, pk: String, from: String, to: String, amount: i64, } async fn transfer_between_accounts( client: &CosmosClient, accounts: &ContainerClient, audit: &ContainerClient, from: Account, to: Account, from_etag: azure_core::http::Etag, to_etag: azure_core::http::Etag, ) -> azure_data_cosmos::Result<()> { let amount = 100; let updated_from = Account { balance: from.balance - amount, ..from }; let updated_to = Account { balance: to.balance + amount, ..to }; let write_transaction = DistributedWriteTransaction::new() .replace_item( accounts, PartitionKey::from(updated_from.pk.clone()), updated_from.id.clone(), &updated_from, Some( DistributedTransactionOperationOptions::default() .with_precondition(Precondition::if_match(from_etag)), ), )? .replace_item( accounts, PartitionKey::from(updated_to.pk.clone()), updated_to.id.clone(), &updated_to, Some( DistributedTransactionOperationOptions::default() .with_precondition(Precondition::if_match(to_etag)), ), )? .create_item( audit, PartitionKey::from("transfers"), "transfer-0001", AuditEvent { id: "transfer-0001".to_owned(), pk: "transfers".to_owned(), from: updated_from.id.clone(), to: updated_to.id.clone(), amount, }, None, )?; let write_response = client .commit_distributed_write(write_transaction) .await?; if write_response.is_completed_status_code() { tracing::info!( idempotency_token = %write_response.idempotency_token(), activity_id = ?write_response.activity_id(), request_charge = ?write_response.request_charge(), "distributed transaction committed" ); return Ok(()); } for index in 0..write_response.len() { if let Some(result) = write_response.operation_result(index) { if result.status_code().as_u16() == 424 { continue; } tracing::warn!( operation_index = result.index(), status = ?result.status_code(), sub_status = ?result.sub_status_code(), pk_range = ?result.partition_key_range_id(), "distributed transaction operation failed" ); } } if write_response.is_retriable() { tracing::warn!( idempotency_token = %write_response.idempotency_token(), diagnostic = ?write_response.diagnostic_string(), "distributed transaction ended retriable after SDK retry budget; reconcile before retrying" ); } Err(azure_data_cosmos::Error::message(format!( "distributed transaction failed: status={:?}, diagnostic={:?}, error={:?}", write_response.status(), write_response.diagnostic_string(), write_response.error_message(), ))) } async fn conditional_patch_example( client: &CosmosClient, container: &ContainerClient, ) -> azure_data_cosmos::Result<()> { let patch = PatchInstructions::from(vec![ PatchOperation::set("/status", serde_json::json!("completed")), ]); let write_transaction = DistributedWriteTransaction::new() .patch_item( container, PartitionKey::from("order-123"), "order-123", patch, Some( DistributedTransactionPatchOperationOptions::default() .with_filter_predicate("from c where c.status = 'pending'"), ), )?; let response = client .commit_distributed_write(write_transaction) .await?; if !response.is_completed_status_code() { for index in 0..response.len() { if let Some(result) = response.operation_result(index) { if result.status_code().as_u16() != 424 { tracing::warn!( index = result.index(), status = ?result.status_code(), sub_status = ?result.sub_status_code(), "patch DTX operation failed" ); } } } } Ok(()) } async fn distributed_read_example<T>( client: &CosmosClient, container_a: &ContainerClient, container_b: &ContainerClient, ) -> azure_data_cosmos::Result<(Option<T>, Option<T>)> where T: serde::de::DeserializeOwned, { let read_transaction = DistributedReadTransaction::new() .read_item( container_a, PartitionKey::from("tenant-1"), "item-1", None, ) .read_item( container_b, PartitionKey::from("tenant-2"), "item-2", None, ); let response = client .execute_distributed_read(read_transaction) .await?; let first = response .operation_result(0) .map(|result| result.resource::<T>()) .transpose()? .flatten(); let second = response .operation_result(1) .map(|result| result.resource::<T>()) .transpose()? .flatten(); Ok((first, second)) } --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: FabianMeiswinkel <19165014+FabianMeiswinkel@users.noreply.github.com>
Addresses review comments on the Distributed Transactions REST wire contract doc: - Corrects MaxCumulativeRetryDelay from 120 s to 30 s (table + prose) and the resulting retry-count estimate (~4-5 retries). - Corrects the response ETag field name to Etag (matches DistributedTransactionSerializer.ResponseETag). - Notes id is required/always emitted in the request body. - Notes localLsn is emitted by the coordinator but not consumed by the SDK. - Adds .ToResourceTypeString() note to the resource-type header row. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f20581f3-35a0-41c0-a3f2-e3b91dfd3fba
Meghana Palaparthi (Meghana-Palaparthi)
previously approved these changes
Jul 17, 2026
Removes all PR-number callouts and folds merged behavior (conditional patch, response reordering + fail-closed, bodyless throttle fall-through) into the core contract prose. Drops the still-open stricter session-token note since it is not yet shipped. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f20581f3-35a0-41c0-a3f2-e3b91dfd3fba
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f20581f3-35a0-41c0-a3f2-e3b91dfd3fba
Meghana Palaparthi (Meghana-Palaparthi)
approved these changes
Jul 17, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Adds the authoritative wire-contract document describing the I/O interactions between
CosmosClientand the Distributed Transactions Coordinator (DTC).Covers request/response headers, request payloads, the read-transaction aggregate status model, and the 408 / 429 / 449 retry policies as implemented by
CosmosClient(verified against SDK source and the backend DTC coordinator).