Normalize region names passed as preferred or exclude regions.#49090
Conversation
Customers passing region names in non-canonical forms (e.g., 'west us 3' instead of 'West US 3') hit routing issues because the Java SDK stores region names in different forms and some comparisons use case-sensitive String.equals()/List.contains(). Changes: - Add RegionNameMapper: strips spaces + case-insensitive lookup against 90+ known Azure regions to produce canonical names (e.g., 'westus3' or 'west us 3' -> 'West US 3'). Unknown regions pass through as-is. - ConnectionPolicy.setPreferredRegions(): normalize + order-preserving dedupe at entry point. - LocationCache constructor: apply RegionNameMapper before toLowerCase for defense-in-depth. - Fix case-sensitive List.contains() bug in reevaluate() (line 502): use containsRegionIgnoreCase() instead. - Normalize user-configured exclude regions at point of use in getApplicableRegionRoutingContexts() to prevent mismatches with PPCB-derived lowercased region names. - Add RegionNameMapperTest with 43 unit tests covering case variants, space removal, passthrough, null/empty handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The static region list in RegionNameMapper goes stale when new Azure regions are added. Fix: add a ConcurrentHashMap-backed dynamic tier that learns canonical region names from server responses. - RegionNameMapper.registerRegionName(): registers canonical names from DatabaseAccountLocation (called from LocationCache.addRoutingContexts). After the first account read, even new regions like 'West US 4' can normalize 'westus4' → 'West US 4'. - getCosmosDBRegionName(): checks static map first, then dynamic map. - Add 2 new tests for dynamic registration behavior. - 45/45 RegionNameMapperTest pass, 32/32 LocationCacheTest pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous commit had stash conflict markers (<<<<<<< Updated upstream / >>>>>>> Stashed changes) left in RegionNameMapper.java and RegionNameMapperTest.java. Rewrote both files clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merge the separate RegionNameMapper into RegionNameToRegionIdMap as the single source of truth for region names. This eliminates maintaining two parallel region lists that can drift out of sync. Changes: - Delete RegionNameMapper.java — normalization logic moved into RegionNameToRegionIdMap. - RegionNameToRegionIdMap now provides region ID mapping (existing) AND region name normalization (new) from one canonical list. - Sync REGION_NAME_TO_REGION_ID_MAPPINGS with backend RegionToIdMap.cs: add Bleu France Central/South (107/108), Delos Cloud Germany Central/North (109/110), Singapore Central/North (111/112), fix 'easteurope' → 'East Europe' (54). - Build NORMALIZED_REGION_NAME_TO_REGION_ID_MAPPINGS programmatically from REGION_NAME_TO_REGION_ID_MAPPINGS instead of manual duplication. - Normalization static map seeded from ID map keys + additional regions without IDs yet (from .NET SDK Regions.cs). - Rename test: RegionNameMapperTest → RegionNameToRegionIdMapNormalizationTest. - Update ConnectionPolicy and LocationCache references. - All 78 tests pass (45 normalization + 32 LocationCache + 1 consistency). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 7 tests to LocationCacheTest using real Azure region names to verify that preferred regions and exclude regions work correctly with non-canonical input: - preferredRegions_lowercaseShouldMatchCanonical: 'west us 3' → West US 3 - preferredRegions_noSpacesShouldMatchCanonical: 'westus3' → West US 3 - preferredRegions_uppercaseShouldMatchCanonical: 'WEST US 3' → West US 3 - preferredRegions_duplicateAfterNormalizationShouldDedupe: 'westus3' + 'West US 3' deduped to single entry - excludeRegions_lowercaseNoSpacesShouldExclude: 'westus3' excludes West US 3 - excludeRegions_mixedCasingShouldExclude: 'EAST us' excludes East US - excludeRegions_requestLevelNoSpacesShouldExclude: request-level 'eastus' excludes East US All 39 LocationCacheTest unit tests pass (32 existing + 7 new). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create a second CosmosClient with space-stripped preferred regions (e.g., 'westus3' instead of 'west us 3') and verify that routing and region exclusion work identically to canonical names. New tests: - nonCanonicalPreferredRegions_shouldRouteCorrectly: client with space-stripped preferred regions routes to correct first region (7 operation types via DataProvider) - nonCanonicalExcludeRegion_shouldSkipExcludedRegion: excluding with space-stripped name (e.g., 'westus3') correctly skips that region (7 operation types via DataProvider) - uppercaseExcludeRegion_shouldSkipExcludedRegion: excluding with UPPERCASE name correctly skips that region Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add tests that create CosmosClients with space-stripped preferred regions (e.g., 'westus3' instead of 'West US 3') and verify correct routing. FaultInjectionWithAvailabilityStrategyTestsBase: - Add nonCanonicalWriteableRegions field (space-stripped from server names) - readAfterCreation_nonCanonicalPreferredRegions_shouldRouteCorrectly: creates client with space-stripped regions, reads with eager availability strategy, verifies first contacted region matches expected canonical name PerPartitionCircuitBreakerE2ETests: - nonCanonicalPreferredRegions_ppcbShouldStillRouteCorrectly: creates client with space-stripped regions, performs create+read, verifies diagnostics show routing to correct first preferred region Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Simplify RegionNameToRegionIdMap by removing the ConcurrentHashMap-backed dynamic registration tier. Unknown regions are returned as-is, which is sufficient because LocationCache's CaseInsensitiveMap + toLowerCase handles the matching for any region the server returns. - Remove DYNAMIC_NORMALIZED_TO_CANONICAL and registerRegionName() - Remove registerRegionName() call from LocationCache.addRoutingContexts() - Replace dynamic registration tests with passthrough assertion tests - 84/84 tests pass (44 normalization + 39 LocationCache + 1 consistency) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sible Duplicate preferred regions after normalization (e.g., ['westus3', 'West US 3'] both becoming 'West US 3') are an obvious customer misconfiguration. The SDK should not silently mask this — let the duplicates pass through so the customer can see and fix their config. Also clarify code comments for the escape hatch behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 10 regions from the authoritative LocationNames.cs that were missing from the normalization map: East US SLV, Southeast US, Southwest US, South Central US 2, Southeast US 3, Southeast US 5, Northeast US 5, India South Central, Southeast Asia 3, West Central US FRE. Region ID mappings remain a subset (only regions with assigned IDs from RegionToIdMap.cs). The normalization map is the superset sourced from LocationNames.cs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the previous RegionToIdMap.cs-based ID map with the complete regionToIdMapping from Settings.xml (IDs 1-124). This is the authoritative source for region name ↔ ID mappings used for session token region-level progress tracking. - Add 44 new region IDs (74-124): Brazil Southeast, West US 3, Qatar Central, Italy North, East US 3, Saudi Arabia East, etc. - Remove separate 'additional canonical names' block — all canonical names now derive from the ID map since Settings.xml is the superset. - Remove 'Greece Central' which was not in any authoritative source. - Update javadoc and code comments to reference Settings.xml as the authoritative source instead of RegionToIdMap.cs. - 83/83 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ationCache - Rename class to RegionUtils — better reflects its dual role (ID mapping + region name normalization). - Move normalizeRegionNames() and containsRegionIgnoreCase() from LocationCache private helpers into RegionUtils as public static methods. - Rename all test files to match: RegionUtilsNormalizationTest, RegionUtilsTests. - Update all references across ConnectionPolicy, LocationCache, PartitionScopedRegionLevelProgress, RxDocumentClientImpl. - 83/83 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/azp run java - cosmos - tests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
@sdkReviewAgent |
There was a problem hiding this comment.
Pull request overview
This PR addresses Cosmos DB routing mismatches caused by non-canonical Azure region name inputs by introducing centralized region normalization and applying it to preferred/excluded region handling across the Cosmos Java SDK routing stack.
Changes:
- Added
RegionUtilsas the single source of truth for region ID mappings and canonical region name normalization, and updated call sites to use it. - Normalized preferred/excluded regions in
ConnectionPolicyandLocationCache, including a fix for a case-sensitive exclude-region check in PPCB reevaluation logic. - Added/updated unit and E2E tests to validate routing behavior with non-canonical region inputs and updated the Cosmos changelog entry.
Reviewed changes
Copilot reviewed 13 out of 13 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 | Switches region ID lookup to RegionUtils. |
| sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/routing/RegionUtils.java | Introduces region normalization + region ID mapping utilities. |
| sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/routing/RegionNameToRegionIdMap.java | Removes the old region mapping class in favor of RegionUtils. |
| sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/routing/LocationCache.java | Normalizes excluded regions and fixes PPCB exclude-region comparison behavior. |
| sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/PartitionScopedRegionLevelProgress.java | Updates region ID/name lookups to RegionUtils. |
| sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/ConnectionPolicy.java | Normalizes preferred regions at configuration time. |
| sdk/cosmos/azure-cosmos/CHANGELOG.md | Documents the normalization + PPCB exclude-region fix. |
| sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/PerPartitionCircuitBreakerE2ETests.java | Adds E2E coverage for PPCB routing with non-canonical preferred regions. |
| sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/routing/RegionUtilsNormalizationTest.java | Adds unit coverage for normalization behavior. |
| sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/routing/LocationCacheTest.java | Adds integration-style unit tests for preferred/exclude region normalization with real region names. |
| sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/RegionUtilsTests.java | Updates the existing mapping-consistency test to the new RegionUtils. |
| sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/FaultInjectionWithAvailabilityStrategyTestsBase.java | Adds E2E validation for availability strategy routing with non-canonical preferred regions. |
| sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/ExcludeRegionTests.java | Adds E2E coverage for non-canonical preferred/exclude region inputs. |
|
@sdkReviewAgent |
|
/azp run java - cosmos - kafka |
|
Azure Pipelines successfully started running 1 pipeline(s). |
DR Drill (Account-level actions)Branch: Verdict: PASSAll 6 tenants on both accounts connected, served traffic from their preferred region in steady state, and responded correctly to both DR events. Zero user-visible failures; transient errors during failover (404/1002, 403/3, 403/1008) auto-retried by Timeline (UTC)
Workload matrix (1 variant per slot, same on both accounts, runs full duration)
User-agent suffix pattern: Expected DR behavior (interpreting the charts)
For Source-of-truth Kusto queriesRun against Q1 -- Per-variant routing over time (Direct mode)BackendEndRequest5M
| where TIMESTAMP between (datetime(2026-06-01T00:40Z) .. datetime(2026-06-01T02:00Z))
| where GlobalDatabaseAccountName == 'mr-drill'
| where UserAgent has 'dr-mr-'
| where ResourceType == 2 and StatusCode < 400
| extend Workload = extract('(dr-mr-[a-zA-Z-]+)', 1, UserAgent)
| where Workload contains '-d-'
| summarize Requests = sum(SampleCount) by bin(TIMESTAMP, 5m), Region, Workload
| order by Workload asc, TIMESTAMP asc
| render timechartResult (mr-drill): Result (mwr-drill): Q2 -- Per-variant routing over time (Gateway mode)Request5M
| where TIMESTAMP between (datetime(2026-06-01T00:40Z) .. datetime(2026-06-01T02:00Z))
| where globalDatabaseAccountName == 'mr-drill'
| where userAgent has 'dr-mr-'
| extend Workload = extract('(dr-mr-[a-zA-Z-]+)', 1, userAgent)
| where Workload contains '-g-'
| where statusCode < 400
| summarize Requests = sum(SampleCount) by bin(TIMESTAMP, 5m), region, Workload
| order by Workload asc, TIMESTAMP asc
| render timechartResult (mr-drill): Result (mwr-drill): Q3 -- MgmtDatabaseAccountTrace (ground-truth region transitions)Confirms the DR actions actually changed region roles. Expect ~10-12 rows per account across the drill. MgmtDatabaseAccountTrace
| where TIMESTAMP between (datetime(2026-06-01T00:40Z) .. datetime(2026-06-01T02:00Z))
| where GlobalDatabaseAccount == 'mr-drill'
| project TIMESTAMP, Location, LocationType, FederationId, Status
| order by TIMESTAMP ascQ4 -- Error breakdown by Status/SubStatus (Direct mode)Should be exclusively transient codes the SDK retries:
BackendEndRequest5M
| where TIMESTAMP between (datetime(2026-06-01T00:40Z) .. datetime(2026-06-01T02:00Z))
| where GlobalDatabaseAccountName == 'mr-drill'
| where UserAgent has 'dr-mr-'
| where ResourceType == 2 and StatusCode >= 400
| extend Workload = extract('(dr-mr-[a-zA-Z-]+)', 1, UserAgent)
| summarize ErrorCount = sum(SampleCount) by bin(TIMESTAMP, 5m), StatusCode, SubStatusCode, Workload
| order by TIMESTAMP ascQ5 -- Success rate by workload (Direct mode)Each workload's line should hover near 100% throughout the drill; temporary dips during the transition buckets are expected (SDK retries before next sampling window). BackendEndRequest5M
| where TIMESTAMP between (datetime(2026-06-01T00:40Z) .. datetime(2026-06-01T02:00Z))
| where GlobalDatabaseAccountName == 'mr-drill'
| where UserAgent has 'dr-mr-'
| where ResourceType == 2
| extend Workload = extract('(dr-mr-[a-zA-Z-]+)', 1, UserAgent)
| where Workload contains '-d-'
| summarize Total = sum(SampleCount), Success = sumif(SampleCount, StatusCode < 400) by bin(TIMESTAMP, 5m), Workload
| extend SuccessRate = round(100.0 * Success / Total, 2)
| project TIMESTAMP, Workload, SuccessRate
| render timechartResult (mr-drill): Result (mwr-drill): Q6 -- Per-variant connectivity sanityThe PR-normalization assertion. Should return at least one row per (workload, region) pair, every row non-zero. A missing variant or row at 0 would mean the SDK failed to resolve that variant's preferred regions = normalization regression. union
(BackendEndRequest5M
| where TIMESTAMP between (datetime(2026-06-01T00:40Z) .. datetime(2026-06-01T02:00Z))
| where GlobalDatabaseAccountName == 'mr-drill' and UserAgent has 'dr-mr-' and ResourceType == 2 and StatusCode < 400
| extend Workload = extract('(dr-mr-[a-zA-Z-]+)', 1, UserAgent)
| where Workload contains '-d-'),
(Request5M
| where TIMESTAMP between (datetime(2026-06-01T00:40Z) .. datetime(2026-06-01T02:00Z))
| where globalDatabaseAccountName == 'mr-drill' and userAgent has 'dr-mr-' and statusCode < 400
| extend Workload = extract('(dr-mr-[a-zA-Z-]+)', 1, userAgent)
| where Workload contains '-g-'
| project-rename Region = region)
| summarize Requests = sum(SampleCount), FirstSeen = min(TIMESTAMP), LastSeen = max(TIMESTAMP) by Workload, Region
| order by Workload asc, Region asc |
|
@sdkReviewAgent |
PPAF DR Drill — Region Name Normalization Validation (Dual Consistency, All Operations)Date: 2026-05-31 (UTC: 2026-06-01) Changes from Drill #2
Test Setup
4 workloads: Strong-DIRECT, Strong-GW, Session-DIRECT, Session-GW Intentionally malformed preferredRegions: Timeline (UTC)
Failover Behavior During QL (01:45–01:55)
Note: Session creates stay on NCU during QL — expected behavior since Session consistency writes go to the write region and can succeed with available replicas. Time Charts — DIRECT Mode (Operation-Level)Strong-DIRECT (BackendEndRequest5M, ResourceType=Document)Session-DIRECT (BackendEndRequest5M, ResourceType=Document)Time Charts — Region-Level (All Workloads, Request5M)Strong-GWSession-GWKey Observations
Success Criteria
All 8/8 criteria PASSED. ✅ |
|
/azp run java - cosmos - tests |
|
/azp run java - cosmos - spark |
|
Azure Pipelines successfully started running 1 pipeline(s). |
1 similar comment
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
✅ Review complete (28:53) Posted 1 inline comment(s). Steps: ✓ context, correctness, cross-sdk, design, history, past-prs, synthesis, test-coverage |
The ad-hoc normalizer (toLowerCase only) preserved hyphens/underscores/spaces, so customer-supplied exclude regions like "west-us-3" or "west_us_3" never matched the server-form "west us 3" returned by GlobalEndpointManager.getRegionName. Excluded regions would silently leak back into the availability strategy speculation order. Switch both the set-build and contains-check sites to RegionNameNormalizer.normalize(...) so the exclusion semantics match the rest of the region-name-mapper feature. Removes the now-unused java.util.Locale import. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/azp run java - cosmos - tests |
|
/azp run java - cosmos - spark |
|
Azure Pipelines successfully started running 1 pipeline(s). |
1 similar comment
|
Azure Pipelines successfully started running 1 pipeline(s). |
Per Annie's suggestion on PR Azure#49090: capacity (n*4)/3+1 ensures the set holds n entries without triggering rehash at the default 0.75 load factor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/azp run java - cosmos - tests |
|
/azp run java - cosmos - spark |
|
Azure Pipelines successfully started running 1 pipeline(s). |
1 similar comment
|
Azure Pipelines successfully started running 1 pipeline(s). |










Problem
Fixes #49094
Customers passing region names in non-canonical forms (
"westus3","WEST US 3","west-us-3","west_us_3") hit routing failures because the SDK stored regions in inconsistent shapes and some code paths used case-sensitiveString.equals()/List.contains()against server-returned names like"west us 3". A related bug in PPCB re-evaluation silently re-added user-excluded regions as retry targets when the case didn't match.Definitions
"West US 3","East US". Sourced fromSettings.xml regionToIdMapping."westus3". So"West US 3","WEST-US-3","west_us_3", and"westus3"all normalize to"westus3".DatabaseAccountLocation.getName().toLowerCase(Locale.ROOT):"west us 3"— lowercase, spaces preserved.Design rationale
Two focused helpers, one source of truth.
RegionIdRegistryowns the canonical ↔ region-id mapping sourced fromSettings.xml.RegionNameNormalizeris a pure normalizer (toLowerCase + strip [-_ ]), decoupled from the registry so any input — including regions not in the SDK's compile-time inventory — normalizes deterministically.Public API preserves customer input.
ConnectionPolicy.setPreferredRegions(...)andCosmosExcludedRegionsstore the customer's raw input verbatim. Getters return what the customer supplied; diagnostics still surface the customer's exact strings and the server-form names already used by existing tooling. All canonicalization is internal toLocationCache.Two-map routing inside
LocationCache. The forward endpoint map is stored in two parallel forms keyed off the sameRegionalRoutingContextvalues:availableXxxRegionalRoutingContextsByRegionName— keyed by server-form ("west us 3"). Unchanged. Preserves the contract used byresolveServiceEndpoint,resolveFaultInjectionEndpoint, the PPCB internal-exclude path, and the publicgetRegionNamediagnostic surface (feedsregionsContacted, OpenTelemetryrntbd.region, client telemetry tags,PartitionLevelAutomaticFailoverInfoJSON).availableXxxRegionalRoutingContextsByNormalizedRegionName— keyed byRegionNameNormalizer.normalize(serverName)("westus3"). Used only for customer-supplied preferred-region lookups ingetPreferredAvailableRoutingContextsandshouldRefreshEndpoints.preferredLocationsis stored in normalized form so the lookup is a constant-time hit.Any input variant — canonical, normalized, hyphenated, underscored, or an unknown region in no-space form — resolves consistently for routing while diagnostics keep returning server-form.
Exclude regions hot path.
ConcurrentHashMap<rawString, normalizedString>(rawToNormalizedExcludeRegionCache, soft-capped at 256 entries) eliminates repeat normalization cost for the steady-state case where customers send the same exclude regions on every request. Beyond the cap, falls back to directRegionNameNormalizer.normalize— still correct, just no caching.getApplicableRegionRoutingContextsbuilds aHashSet<String> normalizedExcludesonce per request (drops nulls) and threads it straight through toreevaluate, so all downstream user-exclude membership checks are O(1)Set.containslookups — no duplicated walks, no helper method to keep in sync.Bug fix: PPCB re-evaluate. Previously compared user-excluded regions with case-sensitive
List.contains(), so"West US 3".contains("west us 3")returnedfalseand a user-excluded region was silently re-added as a retry target when PPCB had also excluded it. Now matched via normalized form on both sides, in both the else-branch (line 544) and the global-fallback branch (line 528).Tests
Unit (
azure-cosmos-tests) — full suite (962 tests) passes. New coverage:RegionIdRegistryTests— public-API behavior including all input variants (canonical, normalized, hyphen, underscore, uppercase, null, empty, unknown).RegionIdRegistrySettingsXmlValidationTest— consistency between the SDK registry andSettings.xml.LocationCacheTest— non-canonical preferred-region matching, exclude-region matching for known and unknown regions, plus 5 tests covering the per-client exclude-region cache (population, reuse, distinct entries, soft-cap fallback, null-safe).ApplicableRegionEvaluatorTest— PPCB re-evaluate regression test that defeats the early-return atLocationCache:440so the user-exclude membership check is actually exercised.E2E (
azure-cosmos-tests) — non-canonical preferred + exclude + fault injection across 7 op types inExcludeRegionTests; non-canonical + eager availability strategy + 503 hedging inFaultInjectionWithAvailabilityStrategyTestsBase; non-canonical + PPCB + 503 circuit-breaker failover inPerPartitionCircuitBreakerE2ETests.Spark connector E2E (
RegionNameNormalizationE2EITest, new) — shared test underazure-cosmos-spark_3/src/test/scala/com/azure/cosmos/spark/, exercised against bothazure-cosmos-spark_3-5_2-12(Spark 3.5 LTS) andazure-cosmos-spark_4-1_2-13(Spark 4.1) via the existingcopy-shared-test-sourcesplumbing. Two scenarios:CosmosDiagnostics, feed back its normalized form, assert routing pins to canonical.preferredRegionsList, assert reads route to the canonical secondary (proving routing moved as a result of normalization).Captured via a test-scoped
CosmosDiagnosticsHandlerregistered throughTestCosmosClientBuilderInterceptor.