[Cosmos] Refresh Hedging Detection API #4558
Conversation
023b20f to
dcbddf2
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds a refreshed spec-only Hedging Detection API contract document for the Rust Cosmos SDK (azure_data_cosmos), intended to replace the previously truncated spec and align with the merged hedging design spec (#4330) plus the cross-SDK contract.
Changes:
- Adds a new Hedging Detection API spec covering contract semantics, proposed API surface, and Rust↔contract mapping.
- Specifies per-dispatch region “reason” via
RequestedRegion { region, reason }/RequestedRegionReason, plus theRetry → OperationRetryrename mechanics. - Includes updated reconciliation against
HedgeDiagnostics, plus test-plan and CHANGELOG draft sections.
Resolve the valid issues raised by the Copilot PR review on Azure#4558: - Crate layering: the accessors are inherent methods on the driver's DiagnosticsContext, but azure_data_cosmos depends on azure_data_cosmos_driver (never the reverse), so the driver cannot return SDK-defined types. The spec now defines RequestedRegion and RequestedRegionReason in the driver and re-exports them from azure_data_cosmos (the same pattern as DiagnosticsContext). Reworked section 5 into a three-option comparison (re-export ExecutionContext, driver-defined+re-exported types, SDK-side extension trait) and reframed the 'No Model Sharing' discussion accordingly. - responded_regions filter: completed_at is set by complete(), timeout(), and fail_transport(), so completed_at.is_some() alone wrongly includes timeouts and transport failures. The filter now also requires !timed_out() and error.is_none(). - Added SHA-pinned permalinks for the section 2 surface inventory and the section 6 HedgeDiagnostics citation, per the tracking issue's auditability criteria. Spec-only; no production code changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve the valid issues raised by the Copilot PR review on Azure#4558: - Crate layering: the accessors are inherent methods on the driver's DiagnosticsContext, but azure_data_cosmos depends on azure_data_cosmos_driver (never the reverse), so the driver cannot return SDK-defined types. The spec now defines RequestedRegion and RequestedRegionReason in the driver and re-exports them from azure_data_cosmos (the same pattern as DiagnosticsContext). Reworked section 5 into a three-option comparison (re-export ExecutionContext, driver-defined+re-exported types, SDK-side extension trait) and reframed the 'No Model Sharing' discussion accordingly. - responded_regions filter: completed_at is set by complete(), timeout(), and fail_transport(), so completed_at.is_some() alone wrongly includes timeouts and transport failures. The filter now also requires !timed_out() and error.is_none(). - Added SHA-pinned permalinks for the section 2 surface inventory and the section 6 HedgeDiagnostics citation, per the tracking issue's auditability criteria. Spec-only; no production code changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
329e71b to
4e5049e
Compare
Reconcile sdk/cosmos/azure_data_cosmos/docs/HEDGING_DETECTION_API_SPEC.md with the now-merged Rust hedging design spec (PR Azure#4330, on release/azure_data_cosmos-previews) and align it with the cross-SDK Hedging Detection API contract. Key updates: - Describe the cross-SDK contract generically (no SDK-specific naming) and add a Rust-to-contract mapping table. - requested_regions() now returns Vec<RequestedRegion> carrying a RequestedRegionReason, conforming to the contract's per-region-reason shape; add the public RequestedRegion struct and RequestedRegionReason enum plus a From<ExecutionContext> mapping. This resolves the previously-deferred per-region-reason question. - Correct the Retry to OperationRetry rename mechanics to the real code: ExecutionContext derives Serialize only (no Deserialize), and the hand-written as_str() must also be updated; serde(alias) is now conditional on adding Deserialize. - Fix the HedgeDiagnostics reconciliation: strategy_config and response_region are non-optional in the merged HEDGING_SPEC.md; fix the hedging_started() doc-comment that called as_ref() on a non-Option. - Correct the surface inventory: real dispatch-site paths, Serialize-only derive, and that regions_contacted() is sorted/deduped (so new accessors are needed for dispatch/completion order with duplicates). - Complete the file, which was truncated mid-section 8.2: finish the test plan and add section 9 (CHANGELOG drafts) and section 10 (open questions). Spec-only; no production code changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve the valid issues raised by the Copilot PR review on Azure#4558: - Crate layering: the accessors are inherent methods on the driver's DiagnosticsContext, but azure_data_cosmos depends on azure_data_cosmos_driver (never the reverse), so the driver cannot return SDK-defined types. The spec now defines RequestedRegion and RequestedRegionReason in the driver and re-exports them from azure_data_cosmos (the same pattern as DiagnosticsContext). Reworked section 5 into a three-option comparison (re-export ExecutionContext, driver-defined+re-exported types, SDK-side extension trait) and reframed the 'No Model Sharing' discussion accordingly. - responded_regions filter: completed_at is set by complete(), timeout(), and fail_transport(), so completed_at.is_some() alone wrongly includes timeouts and transport failures. The filter now also requires !timed_out() and error.is_none(). - Added SHA-pinned permalinks for the section 2 surface inventory and the section 6 HedgeDiagnostics citation, per the tracking issue's auditability criteria. Spec-only; no production code changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
4e5049e to
1c295b3
Compare
Land the Hedging Detection API implementation described by HEDGING_DETECTION_API_SPEC.md in the same PR as the spec, since the change is small enough to combine. Driver (azure_data_cosmos_driver): - Add `OperationRetry` to `ExecutionContext`; deprecate `Retry` (serialized form changes "retry" -> "operation_retry"). Update `as_str()` and all dispatch sites in the operation/transport pipelines. - Add public `RequestedRegion` struct and `RequestedRegionReason` enum (both `#[non_exhaustive]`) plus a total `From<ExecutionContext>` mapping. - Add `DiagnosticsContext::requested_regions()` (dispatch order, duplicates, per-region reason), `responded_regions()` (completion order, service replies only via a new `responded_with_service_reply()` predicate), and `hedging_started()` (ExecutionContext::Hedging predicate; the hedge_diagnostics signal lights up once the hedging implementation lands). - Export the two new types from the diagnostics module. - Add unit tests for the accessors and mapping totality. SDK (azure_data_cosmos): - Re-export `RequestedRegion` / `RequestedRegionReason`, mirroring `CosmosDiagnosticsContext`. Docs/CHANGELOGs: - Reconcile the spec's "spec-only" framing now that the implementation lands together, and add CHANGELOG entries to both crates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1c295b3 to
2c28269
Compare
| - Exposed cross-regional read hedging. Enable it by attaching an `OperationOptions` built with `OperationOptionsBuilder::with_availability_strategy(AvailabilityStrategy::Hedging(HedgingStrategy::new(HedgeThreshold::new(threshold)?)))` to a request (e.g. `ItemReadOptions::with_operation_options`) or to the client defaults via `CosmosClientBuilder::with_operation_options`. The `AvailabilityStrategy`, `HedgingStrategy`, and `HedgeThreshold` types are now re-exported from `azure_data_cosmos`. When enabled, the driver speculatively dispatches the read to a second preferred region after the configured threshold elapses and returns whichever response classifies as final first, cancelling the losing leg structurally (no detached tasks); `AvailabilityStrategy::Disabled` turns hedging off for that scope, and when no strategy is configured the driver applies a built-in default for multi-region reads. ([#4432](https://github.com/Azure/azure-sdk-for-rust/pull/4432)) | ||
|
|
||
| - Added configurable retry limits for throttled (HTTP 429, rate-limited) requests, mirroring the .NET and Java SDKs' `ThrottlingRetryOptions`. A new nested `ThrottlingRetryOptions` group on `OperationOptions` (field `throttling_retry_options`) carries `max_retry_count` (env `AZURE_COSMOS_MAX_THROTTLE_RETRY_COUNT`, default `9`, `0` disables throttle retries) and `max_retry_wait_time` (default `30s`), settable per-request via `OperationOptions`/`OperationOptionsBuilder` and `ThrottlingRetryOptionsBuilder`. New client-wide setter `CosmosClientBuilder::with_throttling_retry_options(ThrottlingRetryOptions)` forwards the group as runtime-layer defaults. Both budgets apply *per transport-pipeline invocation*, not per logical operation — an operation that fans out across regions (failover, hedging) starts a fresh budget per leg; use `OperationOptions::end_to_end_latency_policy` to bound total per-operation wall-clock time. ([#4544](https://github.com/Azure/azure-sdk-for-rust/pull/4544)) | ||
|
|
There was a problem hiding this comment.
Fixed in 5bcd815. With the helper API removed this entry got rescoped too: the driver CHANGELOG now just records the Retry -> OperationRetry rename under Breaking Changes, and the stray blank line is gone.
analogrelay
left a comment
There was a problem hiding this comment.
My personal preference would be to remove all the helpers that dig through the requests and create new data (requested_regions(), hedging_started(), etc.) but I'm not going to block the PR on it :).
| - Added `PartitionKeyDefinition::with_kind(PartitionKeyKind)` and `PartitionKeyDefinition::with_version(PartitionKeyVersion)` consuming setters so callers can override the auto-inferred kind and the default `V2` version without resorting to private-field workarounds. `PartitionKeyDefinition::new(paths)` continues to auto-infer the kind from the path count. | ||
| - Reshaped `PatchInstructions` API so that `PatchInstructions::new()` creates an empty instruction set, where individual operations can be added via `with_operation()`. The `PatchInstructions` type also implements `From<Vec<PatchOperation>>` for ergonomic construction from a vector. | ||
| - Refactored the driver response surface: introduced `ResponseBody` (a `NoPayload` / `Bytes(Bytes)` / `Items(Vec<Bytes>)` enum with `single()`, `items()`, `into_single::<T>()`, `into_items::<T>()`, and `is_empty()` helpers), added typed `CosmosRequestHeaders` fields for query / changefeed headers (`max_item_count`, `incremental_feed`, `populate_index_metrics`, `populate_query_metrics`, `enable_cross_partition_query`) so callers no longer need raw `custom_headers`, the pipeline now auto-emits `x-ms-documentdb-isquery: True` and `Content-Type: application/query+json` for `OperationType::Query`, and `CosmosStatus` gained `PartialEq<StatusCode>`, `From<CosmosStatus> for StatusCode/u16`, and a `CosmosStatus::new(StatusCode)` constructor. ([#4401](https://github.com/Azure/azure-sdk-for-rust/pull/4401)) | ||
|
|
There was a problem hiding this comment.
Fixed in 5bcd815 - removed the stray blank line. The hedging Features entry it followed was also removed (the SDK no longer adds any public surface here, so there is nothing to note in the SDK changelog).
| /// | ||
| /// Renamed to [`ExecutionContext::OperationRetry`] to align with the | ||
| /// cross-SDK reason taxonomy. Retained for one release for source | ||
| /// compatibility; the serialized form also changes from `"retry"` to | ||
| /// `"operation_retry"`. | ||
| #[deprecated(since = "0.3.0", note = "use `ExecutionContext::OperationRetry`")] | ||
| Retry, |
There was a problem hiding this comment.
Nah, just remove it. We'll have breaking changes pre-1.0, no need to deprecate and alias stuff
There was a problem hiding this comment.
Done in 5bcd815 - removed Retry outright (no #[deprecated], no alias). It is renamed straight to OperationRetry, and the serialized form is now operation_retry. Agreed there is no point aliasing pre-1.0.
| /// wildcard arm. | ||
| #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | ||
| #[non_exhaustive] | ||
| pub enum RequestedRegionReason { |
There was a problem hiding this comment.
Seems like this could just be ExecutionContext rather than defining a separate enum. I've never been a huge fan of the ExecutionContext name anyway :). Maybe RequestPurpose? RequestIntent?
There was a problem hiding this comment.
Agreed - removed the parallel RequestedRegionReason (and RequestedRegion) entirely in 5bcd815. ExecutionContext is now the single source of truth for a request's reason; callers read it directly off RequestDiagnostics::execution_context(), so there is no second enum to keep in sync. I left renaming ExecutionContext itself (-> RequestPurpose / RequestIntent) out of this PR to keep the blast radius small and called it out as future work in the spec - happy to do it here instead if you would prefer.
| pub fn requested_regions(&self) -> Vec<RequestedRegion> { | ||
| self.requests | ||
| .iter() | ||
| .filter_map(|r| { | ||
| r.region().map(|region| RequestedRegion { | ||
| region: region.clone(), | ||
| reason: RequestedRegionReason::from(r.execution_context()), | ||
| }) | ||
| }) | ||
| .collect() | ||
| } |
There was a problem hiding this comment.
I'm not a huge fan of defining all these helpers before we know exactly what customers are going to do with them. In Rust, this causes allocations that a user may prefer to avoid. Of course, they can just iterate the requests themselves, but having our own helper here just feels unnecessary. IMO, just let the user dig into the requests for now. When we're generating diagnostic strings or possibly concise, loggable, descriptions of diagnostics output, it might make sense to calculate this, but for now I don't feel a need to add it. I'd rather be conservative with public API in this fresh v1.
There was a problem hiding this comment.
Agreed, and thanks - removed requested_regions(), responded_regions(), and hedging_started() (plus the responded_with_service_reply predicate) in 5bcd815. Callers now dig into the already-public requests() / hedge_diagnostics() themselves and only allocate when they actually want a derived collection. The spec was rewritten to document the detection recipes instead of shipping helpers, with a note that we can add helpers later (e.g. when generating loggable diagnostic strings) once we know how customers use them.
Per reviewer feedback on PR Azure#4558, be conservative with the v1 public surface instead of shipping computed convenience helpers. - Remove DiagnosticsContext::requested_regions/responded_regions/ hedging_started and the RequestDiagnostics::responded_with_service_reply predicate. Callers read the already-public requests()/hedge_diagnostics() building blocks directly (allocating only when they need to). - Remove the parallel RequestedRegion / RequestedRegionReason types and the From<ExecutionContext> mapping; ExecutionContext is the single source of truth for a request's reason. Drop their driver and SDK re-exports. - Remove the deprecated ExecutionContext::Retry alias; pre-1.0 we rename outright to OperationRetry (serialized "retry" -> "operation_retry"). - Rewrite HEDGING_DETECTION_API_SPEC.md around the building-block approach, documenting detection recipes rather than helper methods. - Fix stray CHANGELOG newlines and rescope entries to the rename only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s-hedging-pr-review
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@analogrelay thanks for the review. I took your personal preference and ran with it - rather than ship the convenience helpers, I removed them and reworked the PR around the alternate "let callers read the raw diagnostics" approach. What changed (commit 5bcd815, plus a merge of
Merge conflicts with |
|
Note: holding this PR - please do not merge yet. I want to block it until my later diagnostics changes are checked in, so we can land them together and avoid churn on the diagnostics surface. |
The two delimiter rows in the spec's tables used compact pipes (|---|---|---|) while their header and body rows are space-padded, tripping MD060/table-column-style and failing the CI Build Analyze stage. Pad the delimiter rows to match the surrounding style. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Pushed 8a0b110 to fix the failing Build Analyze stage: the two table delimiter rows in HEDGING_DETECTION_API_SPEC.md used compact pipes (|---|---|---|) while their header/body rows are space-padded, tripping MD060/table-column-style. Padded the delimiters to | --- | --- | --- |. Verified locally: markdownlint-cli2 now reports 0 errors on the spec, and cspell is clean. The substantive review threads (drop the helper APIs, remove the parallel RequestedRegion/RequestedRegionReason enum, rename Retry -> OperationRetry without an alias, CHANGELOG newline) were already addressed in 5bcd815; the spec already documents the detection recipes against the raw diagnostics building blocks rather than shipping helpers. |
Implements the Hedging Detection API for the Rust Cosmos SDK and lands the accompanying spec (
sdk/cosmos/azure_data_cosmos/docs/HEDGING_DETECTION_API_SPEC.md) in the same PR — the change was small enough to combine. Rebased onto currentmain, which already carries the hedging implementation (#4432) and the hedging design spec (#4330).What's implemented
Driver (
azure_data_cosmos_driver)DiagnosticsContext:requested_regions() -> Vec<RequestedRegion>— dispatch order, duplicates allowed, region-Noneentries skipped, each tagged with aRequestedRegionReason.responded_regions() -> Vec<&Region>— completion order (stable sort bycompleted_at), service replies only (excludes client timeouts and transport failures via a newRequestDiagnostics::responded_with_service_reply()predicate; non-2xx replies still count).hedging_started() -> bool— disjunction ofhedge_diagnostics().alternate_region().is_some()(fan-out permain's landedHedgeDiagnostics) and anyRequestDiagnosticstaggedExecutionContext::Hedging.RequestedRegionstruct andRequestedRegionReasonenum (both#[non_exhaustive]) plus a totalFrom<ExecutionContext>mapping.TransportRetry/CircuitBreakerProbereasons are reserved.ExecutionContext::Retry→ExecutionContext::OperationRetry(old variant kept#[deprecated]for one release; serialized string changes"retry"→"operation_retry"). Updatedas_str()and all dispatch sites.SDK (
azure_data_cosmos)RequestedRegion/RequestedRegionReason, mirroring the existingDiagnosticsContextre-export.Docs / CHANGELOGs
main's reality:DiagnosticsContextnaming (not the previews-eraCosmosDiagnosticsContext),HedgeDiagnosticsshape (terminal_state/alternate_region, nottotal_requests_launched/was_hedge), and the now-landed hedging implementation (Cosmos: Implement Cross Regional Hedging In Driver Crate #4432).Validation
cargo build,cargo clippy, andcargo fmt --checkare clean on both crates; the full driver lib test suite passes (1771 tests), including new unit tests for the three accessors and theFrom<ExecutionContext>mapping totality. No new markdownlint errors introduced.Refs #4410.
cc @kundadebdatta @analogrelay @FabianMeiswinkel @simorenoh