Add metadata hedging for the Python Cosmos SDK (port of Azure/azure-cosmos-dotnet-v3#5999)#47918
Add metadata hedging for the Python Cosmos SDK (port of Azure/azure-cosmos-dotnet-v3#5999)#47918NaluTripician wants to merge 7 commits into
Conversation
Self-review (Seon thorough) fixes: - Gate hedging on an explicit metadataCachePopulation request flag set only by the container-properties refresh, cold partition-key read, and routing-map fetch callsites, so public container reads and hybrid-search PK reads are no longer hedged. - Size the hedge thread pool to max(cpu_count, 2*budget) so each in-flight hedge always has its primary+hedge threads. - Remove the dead record_failure no-op (metadata cache health is account-global, not per-partition). - Clarify the threshold rationale and the async budget non-blocking check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…an/metadata-hedging-python
Builds on the adopted cold-start metadata hedging base and closes the parity gaps identified in Phase 1 review: - is_regional_failure now includes 410 + LeaseNotFound (new SubStatusCodes.LEASE_NOT_FOUND = 1022), shared by sync + async. - PartitionKeyRange hedging restricted to the first change-feed page; pages 2..N are pinned to the winning region via excludedLocations for continuation integrity, in both sync and async routing-map drains. Winner surfaced through a shared _record_winner helper on AvailabilityStrategyHandlerMixin and a per-request winner sidecar. - AZURE_COSMOS_METADATA_HEDGING_ENABLED env var added as an operator kill-switch that overrides the client opt-in and PPAF state. - Threshold-safety guarded by a test asserting 0 < threshold < DBAReadTimeout. - Corrected reference PR number in module docstrings (#5923 -> #5999). Adds sync + async regression tests (test_metadata_hedging_gaps[_async].py). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e_metadata_hedging Metadata hedging covers both initial cache population and cache refresh (warm path), matching the .NET feature, so the cold-start-specific name and docstrings were too narrow. Renames the public CosmosClient keyword (sync + async) and updates the docstrings/CHANGELOG accordingly. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Ports cold-start metadata-cache cross-region hedging from the .NET Cosmos SDK (Azure/azure-cosmos-dotnet-v3#5999) to the Python SDK. When a metadata read (Collection read or the first PartitionKeyRange change-feed page) is slow on the primary region, the SDK dispatches one hedged request to a second region after a fixed 1.5 s threshold and returns the first acceptable answer, while keeping the primary authoritative. A new additive enable_metadata_hedging_for_cold_start client keyword (tri-state) plus the AZURE_COSMOS_METADATA_HEDGING_ENABLED env kill-switch control the feature, for both sync and async stacks.
Changes:
- New sync/async hedging handlers (
_metadata_hedging.py) with bounded concurrency, regional-failure classification, and shared winner-recording logic in the strategy mixin. - Wiring through client → connection → request dispatch, gated on a new
is_metadata_cache_populationrequest flag; PartitionKeyRange drains hedge only page 1 and pin subsequent pages to the winning region for continuation integrity. - New config (thresholds, env var, tri-state resolver), a
LEASE_NOT_FOUNDsubstatus, CHANGELOG entry, and comprehensive sync/async unit + regression tests.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
azure/cosmos/_metadata_hedging.py |
New sync handler; executor created here is never shut down on client close. |
azure/cosmos/aio/_metadata_hedging.py |
New async handler (asyncio tasks, proper cancel/gather). |
azure/cosmos/_availability_strategy_config.py |
Metadata strategy, env-var kill-switch, tri-state opt-in resolver. |
azure/cosmos/_availability_strategy_handler_base.py |
Shared _record_winner for continuation pinning. |
azure/cosmos/_request_object.py |
New metadataCachePopulation internal option + flag. |
azure/cosmos/_synchronized_request.py |
Sync dispatch + applicability gate. |
azure/cosmos/aio/_asynchronous_request.py |
Async dispatch + applicability gate (missing a blank line before AsynchronousRequest). |
azure/cosmos/_routing/routing_map_provider.py |
Sync first-page hedging + later-page region pinning. |
azure/cosmos/_routing/aio/routing_map_provider.py |
Async first-page hedging + later-page region pinning. |
azure/cosmos/_cosmos_client_connection.py |
Sync connection wiring; creates handler, flags cache-population reads. |
azure/cosmos/aio/_cosmos_client_connection_async.py |
Async connection wiring. |
azure/cosmos/cosmos_client.py |
Sync client keyword + docstring. |
azure/cosmos/aio/_cosmos_client.py |
Async client keyword + docstring. |
azure/cosmos/http_constants.py |
Adds LEASE_NOT_FOUND = 1022 (verified correct). |
CHANGELOG.md |
Feature entry under 4.16.2 (Unreleased). |
tests/test_metadata_hedging*.py (+ _gaps, async) |
Sync/async unit and regression tests. |
| max_workers = max(os.cpu_count() or 1, 2 * budget) | ||
| self._executor = ThreadPoolExecutor(max_workers=max_workers) # pylint: disable=consider-using-with |
There was a problem hiding this comment.
Good catch. Added MetadataCrossRegionHedgingHandler.close() (idempotent, calls self._executor.shutdown(wait=False, cancel_futures=True)) and wired it into CosmosClient.__exit__ alongside the routing-map release, so a disposed client no longer leaks its hedge worker threads. Added a regression test. Fixed in 84d0b20.
… race Self-review parity fix for PR #47918 (port of Azure/azure-cosmos-dotnet-v3#5999). The winner resolution accepted a HEDGE branch for any non-regional, non-auth outcome, so a hedge could win with a definitive error (e.g. 404) or override a primary that had already produced a definitive answer -- violating the .NET "primary is authoritative" invariant (MetadataHedgingStrategy.ResolveWinnerAsync). A hedge may win ONLY by settling first with a 2xx success, and even then never over a primary that already settled non-regional. Rework the sync and async handlers into a single-exit _resolve_winner helper that classifies each settled branch as SUCCESS / REGIONAL_FAILURE / DEFINITIVE and resolves the winner exactly as .NET does. Add sync + async regression tests covering: a hedge definitive error must not win over a slow primary success, must defer to a primary definitive error, and must not override a primary regional failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Self-review pass (parity check vs .NET #5999)Did a local skeptic-lens review of this PR against the .NET reference ( Issue found & fixed — the primary was not fully authoritative. The winner resolution accepted a hedge branch for any non-regional, non-auth outcome, so a hedge could:
That violates the .NET invariant: a hedge may win ONLY by settling first with a 2xx success, and even then never over a primary that already settled with a non-regional outcome. Fix: reworked both the sync and async handlers into a single-exit
Tests: added sync + async regression tests in Reviewed and intentionally left as-is: async budget |
…ments Addresses CI failures and reviewer comments on PR #47918: - consistency: regenerate api.md + api.metadata.yml (apiview-stub-generator 0.3.28) to include the new public surface -- async CosmosClient enable_metadata_hedging keyword, SubStatusCodes.LEASE_NOT_FOUND, and the CosmosClientConnection keyword. - Analyze/pylint: silence too-many-locals on CosmosClientConnection.__init__ (the new keyword pushed it 25->26) and add the do-not-import-asyncio waiver to the rom asyncio import ... line in aio/_metadata_hedging.py. - cspell: add PPAF to the cosmos word list and reword a "falsey" comment. - Review comment: shut down the per-client metadata-hedge ThreadPoolExecutor on client teardown. Add MetadataCrossRegionHedgingHandler.close() (idempotent) and invoke it from CosmosClient.__exit__ so a disposed client no longer leaks its hedge worker threads. Add a regression test. - Review comment: add the missing second blank line before AsynchronousRequest (PEP 8 / black). The failing EmulatorTests are pre-existing flakiness unrelated to this change: different cross-region retry/fault-injection tests fail on different Python versions (test_service_retry_policies on 3.10, test_timeout_and_failover_retry_policy on 3.13), and metadata hedging cannot fire under emulator defaults (no opt-in, PPAF off). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CI fixes + review comments addressed (84d0b20)CI failures
Review comments
Emulator test failures are pre-existing flakiness, not from this PR. Different cross-region retry / fault-injection tests fail on different Python versions — Local validation: 46 metadata-hedging unit tests pass; pylint + mypy clean on the changed modules; cspell clean; api.md diff verified byte-for-byte against a fresh 0.3.28 regeneration. |
Summary
Ports metadata cache cross-region hedging from the .NET SDK (Azure/azure-cosmos-dotnet-v3#5999) to the Python Cosmos SDK. When a metadata read on the primary region is slow, the SDK dispatches a single hedged request to another region after a short threshold and returns the first acceptable answer, so a single slow region no longer stalls metadata cache population or refresh.
Two metadata reads are hedged: Collection Read (container properties) and PartitionKeyRange ReadFeed (first change-feed page only), covering both initial cache population and warm-path cache refresh. Both the sync and async stacks are covered. The primary is always authoritative — a hedge can only win by being faster and producing a good answer; a misconfigured secondary can never surface a spurious result.
Public API
New keyword on
CosmosClient(sync + async), additive and non-breaking:None(default) → follows the account's PPAF (Per-Partition Automatic Failover) stateTrue→ hedges even when PPAF is offFalse→ disabledThe
AZURE_COSMOS_METADATA_HEDGING_ENABLEDenvironment variable is an operator kill-switch that overrides the keyword and PPAF state (parity with .NET).Behavior details
DBAReadTimeoutof 3 s).503,500,403+DatabaseAccountNotFound, and410+LeaseNotFound. Auth failures (401/plain403) are not regional.excludedLocations) when a hedge wins page 1.Tests
test_metadata_hedging_gaps.py/test_metadata_hedging_gaps_async.pycovering:410+LeaseNotFoundclassification, the threshold-safety invariant, the env-var kill-switch, and first-page continuation pinning.pylintclean for the changed files.Notes
Reference implementation: Azure/azure-cosmos-dotnet-v3#5999. (A couple of module docstrings that cited the wrong number —
Azure/azure-cosmos-dotnet-v3#5923— were corrected to #5999.)Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com