-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Region normalization fix #46792
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: hotfix/azure-cosmos_4.14.6
Are you sure you want to change the base?
Region normalization fix #46792
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,11 @@ | |
| location4_endpoint = "https://location4.documents.azure.com" | ||
| refresh_time_interval_in_ms = 1000 | ||
|
|
||
| canonical_location1_name = "East US 2" | ||
| canonical_location2_name = "West US 3" | ||
| canonical_location1_endpoint = "https://eastus2.documents.azure.com" | ||
| canonical_location2_endpoint = "https://westus3.documents.azure.com" | ||
|
|
||
|
|
||
| def create_database_account(enable_multiple_writable_locations): | ||
| db_acc = DatabaseAccount() | ||
|
|
@@ -46,6 +51,20 @@ def refresh_location_cache(preferred_locations, use_multiple_write_locations, co | |
| connection_policy=connection_policy) | ||
| return lc | ||
|
|
||
|
|
||
| def create_database_account_with_canonical_regions(enable_multiple_writable_locations): | ||
| db_acc = DatabaseAccount() | ||
| db_acc._WritableLocations = [ | ||
| {"name": canonical_location1_name, "databaseAccountEndpoint": canonical_location1_endpoint}, | ||
| {"name": canonical_location2_name, "databaseAccountEndpoint": canonical_location2_endpoint}, | ||
| ] | ||
| db_acc._ReadableLocations = [ | ||
| {"name": canonical_location1_name, "databaseAccountEndpoint": canonical_location1_endpoint}, | ||
| {"name": canonical_location2_name, "databaseAccountEndpoint": canonical_location2_endpoint}, | ||
| ] | ||
| db_acc._EnableMultipleWritableLocations = enable_multiple_writable_locations | ||
| return db_acc | ||
|
|
||
| @pytest.mark.cosmosEmulator | ||
| class TestLocationCache: | ||
|
|
||
|
|
@@ -472,5 +491,88 @@ def test_write_fallback_to_global_after_regional_retries_exhausted(self): | |
| final_endpoint = lc.resolve_service_endpoint(write_request) | ||
| assert final_endpoint == location1_endpoint | ||
|
|
||
| def test_preferred_locations_support_normalized_region_names(self): | ||
| # Preferred locations should match account region names even with case/spacing/separator variations. | ||
| lc = refresh_location_cache(["east-us-2", " west_us_3 "], True) | ||
| db_acc = create_database_account_with_canonical_regions(enable_multiple_writable_locations=True) | ||
| lc.perform_on_database_account_read(db_acc) | ||
|
|
||
| write_contexts = lc.get_write_regional_routing_contexts() | ||
| read_contexts = lc.get_read_regional_routing_contexts() | ||
|
|
||
| assert write_contexts[0].get_primary() == canonical_location1_endpoint | ||
| assert write_contexts[1].get_primary() == canonical_location2_endpoint | ||
| assert read_contexts[0].get_primary() == canonical_location1_endpoint | ||
| assert read_contexts[1].get_primary() == canonical_location2_endpoint | ||
|
|
||
| def test_excluded_locations_support_normalized_region_names(self): | ||
|
dibahlfi marked this conversation as resolved.
|
||
| # Excluded locations should filter regions even when normalized names are used. | ||
| connection_policy = documents.ConnectionPolicy() | ||
| connection_policy.ExcludedLocations = ["east-us-2"] | ||
|
|
||
| lc = refresh_location_cache([canonical_location1_name, canonical_location2_name], True, connection_policy) | ||
| db_acc = create_database_account_with_canonical_regions(enable_multiple_writable_locations=True) | ||
| lc.perform_on_database_account_read(db_acc) | ||
|
|
||
| read_request = RequestObject(ResourceType.Document, _OperationType.Read, None) | ||
| write_request = RequestObject(ResourceType.Document, _OperationType.Create, None) | ||
| write_request.excluded_locations = ["west_us_3"] | ||
|
|
||
| assert lc.resolve_service_endpoint(read_request) == canonical_location2_endpoint | ||
| assert lc.resolve_service_endpoint(write_request) == canonical_location1_endpoint | ||
|
|
||
| def test_should_refresh_endpoints_handles_normalized_preferred_region(self): | ||
| # should_refresh_endpoints must match canonical region keys even when the | ||
| # customer's preferred location uses non-canonical spelling. | ||
| lc = refresh_location_cache(["east-us-2"], True) | ||
| db_acc = create_database_account_with_canonical_regions(enable_multiple_writable_locations=True) | ||
| lc.perform_on_database_account_read(db_acc) | ||
|
|
||
| # Most-preferred is already the primary; no background refresh should be triggered. | ||
| assert lc.should_refresh_endpoints() is False | ||
|
|
||
| def test_get_locational_endpoint_normalizes_customer_region_string(self): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Recommendation — Test Coverage: Missing collision-safety test for normalization invariant The PR description explicitly notes: "a more aggressive rule could collapse genuinely different regions like 'East US' and 'East US 2' into the same key." This is the critical design invariant — but no test guards it. All normalization tests use Suggestion: Add a direct unit test for the normalization boundary: from azure.cosmos._location_cache import _normalize_region_name
def test_normalize_preserves_distinct_regions():
# Regions that differ only by trailing digit must stay distinct
assert _normalize_region_name("East US") != _normalize_region_name("East US 2")
assert _normalize_region_name("West US") != _normalize_region_name("West US 2")
assert _normalize_region_name("West US 2") != _normalize_region_name("West US 3")
# Verify exact canonical outputs
assert _normalize_region_name("East US 2") == "eastus2"
assert _normalize_region_name("East US") == "eastus"
assert _normalize_region_name(None) == ""This is cheap to add and protects the most important invariant in the PR. |
||
| # GetLocationalEndpoint is used during bootstrap fallback with the customer-supplied | ||
| # preferred region string. It must produce the canonical regional URL for any | ||
| # accepted normalization variant. | ||
| default_endpoint_url = "https://contoso.documents.azure.com:443/" | ||
| expected_endpoint = "https://contoso-eastus2.documents.azure.com:443/" | ||
|
|
||
| for region_input in ("East US 2", "east us 2", "eastus2", "east-us-2", "east_us_2", " EastUs2 "): | ||
| assert LocationCache.GetLocationalEndpoint(default_endpoint_url, region_input) == expected_endpoint | ||
|
|
||
| def test_unmatched_excluded_locations_warning_is_deduped(self, caplog): | ||
| connection_policy = documents.ConnectionPolicy() | ||
| connection_policy.ExcludedLocations = ["unknown-region"] | ||
| lc = refresh_location_cache([canonical_location1_name], True, connection_policy) | ||
| db_acc = create_database_account_with_canonical_regions(enable_multiple_writable_locations=True) | ||
| with caplog.at_level("WARNING", logger="azure.cosmos.LocationCache"): | ||
| lc.perform_on_database_account_read(db_acc) | ||
| request = RequestObject(ResourceType.Document, _OperationType.Read, None) | ||
| lc.resolve_service_endpoint(request) | ||
| lc.resolve_service_endpoint(request) | ||
| # Simulate a periodic refresh with unchanged topology and config. | ||
| lc.perform_on_database_account_read(db_acc) | ||
|
|
||
| unmatched_logs = [ | ||
| record for record in caplog.records | ||
| if "Ignoring excluded_locations entries" in record.getMessage() | ||
| ] | ||
| assert len(unmatched_logs) == 1 | ||
|
|
||
| def test_unmatched_preferred_locations_warning_is_deduped(self, caplog): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Recommendation — Test Coverage: Warning re-emission on topology change is untested The dedupe key at However, both Suggestion: Add a test that changes the available regions between calls: def test_topology_change_retriggers_mismatch_warning(self, caplog):
lc = refresh_location_cache(["unknown-region"], True)
db_acc1 = create_database_account_with_canonical_regions(True)
db_acc2 = create_database_account_with_canonical_regions(True)
# Mutate topology for the second refresh
db_acc2._ReadableLocations.append(
{"name": "New Region", "databaseAccountEndpoint": "https://newregion.documents.azure.com"}
)
with caplog.at_level("WARNING", logger="azure.cosmos.LocationCache"):
lc.perform_on_database_account_read(db_acc1)
lc.perform_on_database_account_read(db_acc2)
warnings = [r for r in caplog.records if "Ignoring" in r.getMessage()]
assert len(warnings) == 2 # One per distinct topologyThis protects the deliberate dedupe-key design choice against accidental simplification. |
||
| with caplog.at_level("WARNING", logger="azure.cosmos.LocationCache"): | ||
| lc = refresh_location_cache(["unknown-region"], True) | ||
| db_acc = create_database_account_with_canonical_regions(enable_multiple_writable_locations=True) | ||
| lc.perform_on_database_account_read(db_acc) | ||
| # Simulate a periodic refresh with unchanged topology and config. | ||
| lc.perform_on_database_account_read(db_acc) | ||
|
|
||
| unmatched_logs = [ | ||
| record for record in caplog.records | ||
| if "Ignoring preferred_locations entries" in record.getMessage() | ||
| ] | ||
| assert len(unmatched_logs) == 1 | ||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() | ||
| unittest.main() | ||
Uh oh!
There was an error while loading. Please reload this page.