[Cosmos] Metadata Hedging: cross-region hedging for Collection and PartitionKeyRange metadata reads#49745
[Cosmos] Metadata Hedging: cross-region hedging for Collection and PartitionKeyRange metadata reads#49745NaluTripician wants to merge 4 commits into
Conversation
Port of .NET azure-cosmos-dotnet-v3 PR #5999. Adds the primary-authoritative cross-region metadata hedging strategy core, the tri-state opt-in config (Configs.getMetadataHedgingEnabledOverride / getMetadataHedgingThreshold), and 9 passing unit tests. Cache/client wiring and multi-region integration tests follow in a subsequent increment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wires the MetadataHedgingStrategy into RxClientCollectionCache (Collection Read) and RxPartitionKeyRangeCache (PartitionKeyRange routing-map read, region-pinned full-read racing), constructs/injects it in RxDocumentClientImpl (opt-in via Configs override or PPAF state), and emits a 'Metadata Hedge' diagnostics datum (only when a hedge fires). Adds MetadataHedgingIntegrationTests validating the end-to-end hedge against a live multi-region account (both enabled and disabled). Verified: azure-cosmos compiles; 18 unit tests pass (strategy + PK-range cache, no regressions); 2 multi-region integration tests pass against COSMOSDB_MULTI_REGION; checkstyle 0 violations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e hedge, PKRange region pinning, telemetry) Addresses local self-review findings and tightens .NET (PR #5999) parity: - MetadataHedgingStrategy: fire the hedge immediately when the primary settles with a regional failure before the threshold (was always waiting the full threshold), mirroring the .NET 'primary is slow OR hit a regional failure -> hedge' step. Adds a locking unit test. - RxDocumentClientImpl.nonDocumentReadFeedInternal: propagate per-request excluded regions to the request context so the region-pinned PartitionKeyRange hedge branches actually route to distinct regions (previously the excluded regions on the query options were never applied, so both branches hit the same region). - RxPartitionKeyRangeCache: emit a real activityId on the METADATA_HEDGE diagnostics datum instead of null. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Local self-review round (skeptic lens) — applied in
|
There was a problem hiding this comment.
Pull request overview
This PR introduces bounded cross-region “metadata hedging” for Cosmos Java SDK metadata cache reads (collection lookup and partition key range routing-map readfeed) to reduce tail latency when the primary region is slow or regionally failing. The strategy is constructed once per client, injected into the relevant caches, and emits a new diagnostics datum when a hedge actually fires.
Changes:
- Added
MetadataHedgingStrategyand wired it intoRxClientCollectionCacheandRxPartitionKeyRangeCachefromRxDocumentClientImpl. - Added configuration knobs (
Configs) and a new diagnostics datum (METADATA_HEDGE) to surface hedge behavior. - Added unit + multi-region integration coverage and updated the
azure-cosmoschangelog entry.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/RxDocumentClientImpl.java | Constructs and injects the hedging strategy; ensures excluded-regions are honored for non-document readfeeds. |
| sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/MetadataDiagnosticsContext.java | Adds MetadataHedgeDiagnostics and a new METADATA_HEDGE metadata type + serialization. |
| sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java | Adds tri-state enable override + configurable hedging threshold. |
| sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/RxPartitionKeyRangeCache.java | Implements optional region-pinned hedged routing-map reads and emits hedge diagnostics when fired. |
| sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/RxClientCollectionCache.java | Routes collection reads through the hedging strategy and emits hedge diagnostics. |
| sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/MetadataHedgingStrategy.java | New core hedging implementation: eligibility, region selection, race semantics, and error classification. |
| sdk/cosmos/azure-cosmos/CHANGELOG.md | Documents the new cross-region metadata hedging feature and opt-in knobs. |
| sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/caches/MetadataHedgingStrategyTest.java | Unit tests for classifier + hedging race invariants. |
| sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/faultinjection/MetadataHedgingIntegrationTests.java | Multi-region fault-injection validation for PK-range hedging diagnostics behavior. |
| // The PartitionKeyRange ReadFeed treats certain 404 sub-statuses as regional lag (routing map not | ||
| // yet available in this region), consistent with ClientRetryPolicy's PK-range handling. | ||
| private static boolean isPartitionKeyRangeRegionalFailure(int statusCode, int subStatusCode) { | ||
| return statusCode == HttpConstants.StatusCodes.NOTFOUND | ||
| && (subStatusCode == 0 || subStatusCode == HttpConstants.SubStatusCodes.DATABASE_ACCOUNT_NOTFOUND); | ||
| } |
There was a problem hiding this comment.
Good catch. I removed the PartitionKeyRange-specific 404 classifier entirely (both classifyPartitionKeyRangeThrowable and isPartitionKeyRangeRegionalFailure) and now use the single shared classifyThrowable for both metadata reads. This matches the .NET reference (PR #5999), whose IsRegionalFailure has no 404 handling at all and uses one classifier for both paths. Treating any 404 as definitive (never regional) is actually the correct choice here: it upholds the primary-authoritative invariant, because a lagging secondary returning a stale 200 can never override a primary that legitimately returned 404 (e.g. a deleted/recreated collection or a PK-range gone). The previous variant weakened that guarantee, so removing it is both simpler and safer. Fixed in 5c0462b.
| hedgeResult -> { | ||
| if (metaDataDiagnosticsContext != null) { | ||
| metaDataDiagnosticsContext.addMetaDataDiagnostic( | ||
| new MetadataDiagnosticsContext.MetadataHedgeDiagnostics( | ||
| addressCallStartTime, | ||
| Instant.now(), | ||
| request.getActivityId().toString(), | ||
| hedgeResult.isHedgeFired(), | ||
| hedgeResult.isHedgeWon(), | ||
| hedgeResult.getWinningRegion())); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Fixed in 5c0462b. The Collection path now reports the winning branch's actual on-the-wire activityId, read from the winning response's x-ms-activity-id header (hedgeResult.getValue().getResponseHeaders()), falling back to the original request id only when the primary lost with no response (definitive-error path) -- exactly the winning branch's response activityId, fallback to original you suggested. I applied the same correctness fix to the PartitionKeyRange path: each branch now captures its real FeedResponse.getActivityId() and the datum reports the winner's, instead of a throwaway probe id that was never sent.
| /** | ||
| * Bounded, primary-authoritative cross-region hedging for the two hot-path metadata cache reads | ||
| * (Collection Read and PartitionKeyRange ReadFeed first page). This is the Java port of the .NET | ||
| * metadata hedging strategy (Azure/azure-cosmos-dotnet-v3 PR #5999). | ||
| * <p> |
There was a problem hiding this comment.
Fixed in 5c0462b. Updated the class-level javadoc to state that PartitionKeyRange hedging races region-pinned full routing-map reads (each branch reads all its pages from one pinned region), not a first-page-only read -- and noted this is a deliberate divergence from the .NET reference that preserves continuation integrity while achieving the same latency win.
…yId, javadoc) Addresses the three Copilot review comments on PR #49745: - Remove the PartitionKeyRange-specific 404 regional classifier (classifyPartitionKeyRangeThrowable / isPartitionKeyRangeRegionalFailure) and use the single shared classifyThrowable for both metadata reads, matching the .NET reference. A 404 (any sub-status) is now always definitive, so a lagging secondary can never override a primary 404 -- upholding the primary-authoritative invariant instead of weakening it. - Report the winning branch's actual on-the-wire activityId in the METADATA_HEDGE datum: the Collection path uses the winning response's x-ms-activity-id (fallback to the original request id); the PartitionKeyRange path captures each branch's FeedResponse activityId and reports the winner's. Previously the original/probe request id was logged, but clone() regenerates the activityId so that id was never sent. - Correct the class-level javadoc: PartitionKeyRange hedging races region-pinned full routing-map reads (all pages), not a first-page-only read. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addressed review comments + CI status (5c0462b)Copilot review comments (all 3 resolved):
CI failures (both non-code):
Verification: |
Summary
Adds bounded cross-region hedging for the two metadata cache reads that sit on the critical path of nearly every data operation in the Java SDK:
RxClientCollectionCache→ container properties)RxPartitionKeyRangeCache→ routing map)When the primary region is slow on one of these metadata reads, the SDK dispatches a single hedged request to another region after a fixed latency threshold (default 1.5s) and returns the first acceptable answer. This trims the long tail on cold starts (first cache population) and on steady-state cache refreshes (e.g. after a
410/split or lazy expiry) when the primary region is degraded.This is a Java port of .NET Azure/azure-cosmos-dotnet-v3#5999.
The guiding invariant is "the primary is authoritative." A hedge can only make the operation faster; it can never change the outcome the primary would have produced. A slow, failing, or misconfigured secondary region can never surface a spurious result.
Opt-in
AZURE_COSMOS_METADATA_HEDGING_ENABLEDenvironment variable (or the Java-nativeCOSMOS_METADATA_HEDGING_ENABLEDenv var /COSMOS.metadataHedgingEnabledsystem property) overrides:trueforce-enables,falseis a hard kill-switch.COSMOS.metadataHedgingThresholdInMs/COSMOS_METADATA_HEDGING_THRESHOLD_IN_MS.Design principle: the primary is authoritative
404for a deleted/recreated collection,409,412) is returned verbatim — no hedge is fired, so a stale secondary200can never "resurrect" a deleted collection.503,500,410,403 + DatabaseAccountNotFound, transport/network failures, and (for PartitionKeyRange) the regional-lag404sub-statuses. This classifier is aligned with the SDK's existing cross-region retry semantics (ClientRetryPolicy), not the narrower metadata throttle policy.401/ plain403) are not regional, so a misconfigured secondary is treated as a losing hedge.Architecture
MetadataHedgingStrategy(new, package-private)ConfigsgetMetadataHedgingEnabledOverride()(tri-state) +getMetadataHedgingThreshold().RxDocumentClientImplRxClientCollectionCacheRxPartitionKeyRangeCacheMetadataDiagnosticsContextMETADATA_HEDGEdatum (hedgeFired/hedgeWon/winningRegion), emitted only when a hedge fires — no change to existing no-hedge diagnostics baselines.Each branch runs on its own
request.clone()pinned to a single region (viarouteToLocation+ excluded regions, mirroring the data-planegetEffectiveExcludedRegionsForHedging), so the primary and hedge never race on shared request context and inner cross-region retry is suppressed.Scope
Hedged: Collection Read; PartitionKeyRange routing-map read.
Not hedged: data-plane operations (already covered by the availability strategy); address refresh, query plan, database account, and all other metadata reads; single-region accounts (< 2 applicable regions is a no-op).
Backward compatibility
nullstrategy (original path).Testing
Unit —
MetadataHedgingStrategyTest(9, passing): the regional-vs-definitive classifier and the race invariants — primary-fast → no hedge; primary-slow → hedge wins; fast definitive404→ no hedge; primary503→ hedge wins; both branches fail → primary's error returned; slow definitive409beats an in-flight hedge (primary authoritative).Integration —
MetadataHedgingIntegrationTests(multi-region, 2, passing against a live multi-region account): a primary-regionRESPONSE_DELAYis injected on the PartitionKeyRange metadata read while aPARTITION_IS_SPLITTINGfault forces a routing-map refresh. With hedging enabled theMETADATA_HEDGEdatum withhedgeFired=trueappears; with it disabled, no hedge fires.Verification:
azure-cosmosfullmvn installpasses (revapi API-compat clean, javadoc, checkstyle 0 violations); the existingRxPartitionKeyRangeCacheTeststill passes (no regression from the constructor change).Notes
switchOnFirst, because the Java cache materializes all pages viacollectList(); this preserves continuation integrity and is lower-risk while achieving the same latency win.401and safely lose (hedging becomes a no-op for AAD paths). This is called out for follow-up.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com