Skip to content

Normalize region names passed as preferred or exclude regions.#49090

Merged
jeet1995 merged 62 commits into
Azure:mainfrom
jeet1995:squad/region-name-mapper
Jun 1, 2026
Merged

Normalize region names passed as preferred or exclude regions.#49090
jeet1995 merged 62 commits into
Azure:mainfrom
jeet1995:squad/region-name-mapper

Conversation

@jeet1995

@jeet1995 jeet1995 commented May 7, 2026

Copy link
Copy Markdown
Member

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-sensitive String.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

  • Canonical = official Cosmos display form: "West US 3", "East US". Sourced from Settings.xml regionToIdMapping.
  • Normalized = lowercase with spaces, hyphens, and underscores stripped: "westus3". So "West US 3", "WEST-US-3", "west_us_3", and "westus3" all normalize to "westus3".
  • Server-form = DatabaseAccountLocation.getName().toLowerCase(Locale.ROOT): "west us 3" — lowercase, spaces preserved.

Design rationale

Two focused helpers, one source of truth. RegionIdRegistry owns the canonical ↔ region-id mapping sourced from Settings.xml. RegionNameNormalizer is 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(...) and CosmosExcludedRegions store 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 to LocationCache.

Two-map routing inside LocationCache. The forward endpoint map is stored in two parallel forms keyed off the same RegionalRoutingContext values:

  • availableXxxRegionalRoutingContextsByRegionName — keyed by server-form ("west us 3"). Unchanged. Preserves the contract used by resolveServiceEndpoint, resolveFaultInjectionEndpoint, the PPCB internal-exclude path, and the public getRegionName diagnostic surface (feeds regionsContacted, OpenTelemetry rntbd.region, client telemetry tags, PartitionLevelAutomaticFailoverInfo JSON).
  • availableXxxRegionalRoutingContextsByNormalizedRegionName — keyed by RegionNameNormalizer.normalize(serverName) ("westus3"). Used only for customer-supplied preferred-region lookups in getPreferredAvailableRoutingContexts and shouldRefreshEndpoints. preferredLocations is 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.

  • A per-client 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 direct RegionNameNormalizer.normalize — still correct, just no caching.
  • getApplicableRegionRoutingContexts builds a HashSet<String> normalizedExcludes once per request (drops nulls) and threads it straight through to reevaluate, so all downstream user-exclude membership checks are O(1) Set.contains lookups — 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") returned false and 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 and Settings.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 at LocationCache:440 so the user-exclude membership check is actually exercised.
  • E2E (azure-cosmos-tests) — non-canonical preferred + exclude + fault injection across 7 op types in ExcludeRegionTests; non-canonical + eager availability strategy + 503 hedging in FaultInjectionWithAvailabilityStrategyTestsBase; non-canonical + PPCB + 503 circuit-breaker failover in PerPartitionCircuitBreakerE2ETests.

  • Spark connector E2E (RegionNameNormalizationE2EITest, new) — shared test under azure-cosmos-spark_3/src/test/scala/com/azure/cosmos/spark/, exercised against both azure-cosmos-spark_3-5_2-12 (Spark 3.5 LTS) and azure-cosmos-spark_4-1_2-13 (Spark 4.1) via the existing copy-shared-test-sources plumbing. Two scenarios:

    1. Probe round-trip — discover the SDK's default canonical region from CosmosDiagnostics, feed back its normalized form, assert routing pins to canonical.
    2. Differential — pick a readable region distinct from the probe default, normalize and feed as preferredRegionsList, assert reads route to the canonical secondary (proving routing moved as a result of normalization).

    Captured via a test-scoped CosmosDiagnosticsHandler registered through TestCosmosClientBuilderInterceptor.

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>
@github-actions github-actions Bot added the Cosmos label May 7, 2026
jeet1995 and others added 7 commits May 7, 2026 10:12
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>
@jeet1995 jeet1995 changed the title Port RegionNameMapper from .NET SDK to normalize region names [Cosmos][WIP]: Normalize region names passed as preferred or exclude regions. May 7, 2026
jeet1995 and others added 5 commits May 7, 2026 12:21
…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>
@jeet1995

jeet1995 commented May 7, 2026

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - tests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jeet1995 jeet1995 marked this pull request as ready for review May 7, 2026 17:37
@jeet1995 jeet1995 requested a review from kirankumarkolli as a code owner May 7, 2026 17:37
Copilot AI review requested due to automatic review settings May 7, 2026 17:37
@jeet1995 jeet1995 requested review from a team as code owners May 7, 2026 17:37
@jeet1995

jeet1995 commented May 7, 2026

Copy link
Copy Markdown
Member Author

@sdkReviewAgent

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 RegionUtils as 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 ConnectionPolicy and LocationCache, 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.

@xinlian12

Copy link
Copy Markdown
Member

@sdkReviewAgent

@jeet1995

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - kafka

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jeet1995

jeet1995 commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

DR Drill (Account-level actions)

Branch: squad/region-name-mapper @ 26c99f4999f
Drill window: 2026-06-01 00:42 -> 01:57 UTC (one continuous benchmark per account spanning both rounds)
Accounts: mr-drill (single-writer, EUS2+SCUS), mwr-drill (multi-write, EUS2+SCUS)
Workload: 6 tenants per account (3 ops x 2 modes), each running continuously across both DR rounds with a single non-normalized preferredRegionsList variant

Verdict: PASS

All 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 ClientRetryPolicy.java. Hyphen and underscore variants directly exercised PR commit 7983f09a ("Normalize hyphens and underscores in region names").

Timeline (UTC)

Time Event Notes
00:42:00 benchmark-start one continuous benchmark per account, in parallel
00:47:30 R1 switch-write-region -> SCUS failover-priority-change, both accts
01:05:17 R1 restore-write-region -> EUS2 failover-priority-change, both accts
01:18:13 R2 offline-write-region EUS2 offlineRegion REST, both accts
01:50:32 / 01:52:04 R2 restore-offline EUS2 3-step rebuild (mr-drill / mwr-drill)
01:57:50 benchmark-stop

Workload matrix (1 variant per slot, same on both accounts, runs full duration)

Slot Mode Op Variant preferredRegionsList
1 Direct Read canonical "East US 2, South Central US"
2 Direct Write hyphenLower "east-us-2, south-central-us"
3 Direct Query underscoreUpper "EAST_US_2, SOUTH_CENTRAL_US"
4 Gateway Read allLower "eastus2, southcentralus"
5 Gateway Write upperSpaced "EAST US 2, SOUTH CENTRAL US"
6 Gateway Query trailingWhitespace " East US 2 , south central us "

User-agent suffix pattern: dr-<mr|mwr>-<d|g>-<r|w|q>-<variant>. The benchmark's applicationName config field is propagated as the SDK userAgentSuffix, visible in the Kusto UserAgent / userAgent columns.

Expected DR behavior (interpreting the charts)

Round Action Reads/Queries Writes
R1 (switch) EUS2 demoted to read-only, SCUS promoted to write Stay on EUS2 (reads not affected by write switch) Flip EUS2 -> SCUS at 00:47, back at 01:05
R2 (offline) EUS2 region offlined entirely Flip EUS2 -> SCUS at 01:18, back at 01:50/52 Flip EUS2 -> SCUS at 01:18, back at 01:50/52

For mwr-drill (multi-write), R1 may NOT cause writes to flip since both regions remain writable; SDK preferred-region selection keeps writes on priority-0 (EUS2).


Source-of-truth Kusto queries

Run against cdbsupport.kusto.windows.net / Support database. Pin time range to 2026-06-01T00:40Z -> 2026-06-01T02:00Z for all queries. Swap mr-drill <-> mwr-drill to inspect the other account.

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 timechart

Result (mr-drill):

mr-drill Q1 Direct routing

Result (mwr-drill):

mwr-drill Q1 Direct routing

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 timechart

Result (mr-drill):

mr-drill Q2 Gateway routing

Result (mwr-drill):

mwr-drill Q2 Gateway routing

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 asc

Q4 -- Error breakdown by Status/SubStatus (Direct mode)

Should be exclusively transient codes the SDK retries:

  • 404/1002 ReadSessionNotAvailable -- session token not yet replicated to new region; dominant during failover transitions.
  • 403/3 ForbiddenWriteToReadOnly -- write attempt to former write region after switch.
  • 403/1008 PartitionMigrating -- partitions moving during region offline/online.
  • 429/3200 TooManyRequests -- throttle, exponential-backoff retried.
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 asc

Q5 -- 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 timechart

Result (mr-drill):

mr-drill Q5 Success rate

Result (mwr-drill):

mwr-drill Q5 Success rate

Q6 -- Per-variant connectivity sanity

The 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

@jeet1995

jeet1995 commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

@sdkReviewAgent

@jeet1995

jeet1995 commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

PPAF DR Drill — Region Name Normalization Validation (Dual Consistency, All Operations)

Date: 2026-05-31 (UTC: 2026-06-01)
Branch: squad/region-name-mapper @ commit 26c99f4
JAR: cosmos v4.81.0-beta.1 (fat JAR, built 2026-05-31)
Environment: Test61 (internal staging)

Changes from Drill #2

  • Fixed config asymmetry: All 4 workloads now run ChangeFeed (previously only Session had it enabled)
  • Pushed fix: jeet1995/ppaf-dr-drill@6ede08f

Test Setup

Account Consistency Regions PPAF
abhm-t61-0531-strong-3r Strong NCU (write) → EU2 → CU Enabled
abhm-t61-0531-session-3r Session NCU (write) → EU2 → CU Enabled

4 workloads: Strong-DIRECT, Strong-GW, Session-DIRECT, Session-GW

Intentionally malformed preferredRegions: "NORTHCENTRALUS,East us 2,central-us"
(tests ALL_CAPS, Title Case with space, lowercase-with-dashes — all non-normalized)

Timeline (UTC)

Time (UTC) Event
01:32 All 4 workloads started (Create, Read, Query, ChangeFeed)
01:32–01:45 Pre-QL: all ops routing to NCU ✅
01:45:03 Quorum loss injected on all 4 data partitions (600s, AllReplicas)
01:45:09 Strong-DIRECT: first PPAF failover mark (6s detection)
~01:47 All workloads stabilized on failover regions
01:55:03 QL expired
~01:56 Session fully failed back to NCU
~02:03 Strong fully failed back to NCU
02:12 All workloads completed cleanly

Failover Behavior During QL (01:45–01:55)

Workload Create Read Query ChangeFeed
Strong-DIRECT → EU2 ✅ → EU2 ✅ → EU2 ✅ → EU2 ✅
Strong-GW → EU2 ✅ → EU2 ✅ → EU2 ✅ → EU2 ✅
Session-DIRECT NCU (write region) → EU2 ✅ → EU2 ✅ → EU2 ✅
Session-GW NCU (write region) → EU2 ✅ → EU2 ✅ → EU2 ✅

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)

Strong-DIRECT by Region × Operation

Session-DIRECT (BackendEndRequest5M, ResourceType=Document)

Session-DIRECT by Region × Operation

Time Charts — Region-Level (All Workloads, Request5M)

Strong-GW

Strong-GW by Region

Session-GW

Session-GW by Region

Key Observations

  1. Normalization works: All 3 malformed region formats (NORTHCENTRALUS, East us 2, central-us) normalized correctly — zero IllegalArgumentException errors.
  2. ChangeFeed now runs on ALL workloads: Config asymmetry from Drill Include CHANGELOG #2 is fixed — ReadFeed operations visible on both Strong and Session.
  3. Failover detection: Strong-DIRECT detected QL in 6 seconds (01:45:03 → 01:45:09).
  4. Session creates unaffected: Session writes continue to NCU during QL (expected for Session consistency — writes don't require global quorum).
  5. Strong failback slower: Session failed back in ~1 min post-QL; Strong took ~8 min (expected — Strong needs primary replica recovery).
  6. Zero failures for Session-DIRECT: failureCountUntilNow=0 throughout the entire drill — clean failover and failback.

Success Criteria

# Criterion Result
1 Mixed-format regions accepted without error ✅ PASS
2 Operations route to NCU initially ✅ PASS
3 Reads/Query/ChangeFeed failover to EU2 during QL ✅ PASS
4 Writes failover appropriately during QL ✅ PASS (Strong→EU2, Session stays NCU)
5 Operations resume in NCU after QL ends ✅ PASS
6 No normalization mismatch errors ✅ PASS
7 Strong and Session both behave correctly ✅ PASS
8 Both DIRECT and GATEWAY behave correctly ✅ PASS

All 8/8 criteria PASSED.

@FabianMeiswinkel FabianMeiswinkel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@FabianMeiswinkel

Copy link
Copy Markdown
Member

/azp run java - cosmos - tests

@FabianMeiswinkel

Copy link
Copy Markdown
Member

/azp run java - cosmos - spark

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@kushagraThapar kushagraThapar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@xinlian12

Copy link
Copy Markdown
Member

Review complete (28:53)

Posted 1 inline comment(s).

Steps: ✓ context, correctness, cross-sdk, design, history, past-prs, synthesis, test-coverage

jeet1995 and others added 2 commits June 1, 2026 12:38
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>
@jeet1995

jeet1995 commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - tests

@jeet1995

jeet1995 commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - spark

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
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>
@jeet1995

jeet1995 commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - tests

@jeet1995

jeet1995 commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - spark

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@xinlian12 xinlian12 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks

@FabianMeiswinkel FabianMeiswinkel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jeet1995 jeet1995 merged commit 2acbf43 into Azure:main Jun 1, 2026
109 of 112 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] preferredRegions silently ignores compact Azure region names like westus3 and falls back to default read region

5 participants