diff --git a/Microsoft.Azure.Cosmos/src/Routing/LocationCache.cs b/Microsoft.Azure.Cosmos/src/Routing/LocationCache.cs index 11a1ea4f7b..a0fed64c14 100644 --- a/Microsoft.Azure.Cosmos/src/Routing/LocationCache.cs +++ b/Microsoft.Azure.Cosmos/src/Routing/LocationCache.cs @@ -164,24 +164,24 @@ public ReadOnlyCollection ThinClientWriteEndpoints public ReadOnlyCollection EffectivePreferredLocations => this.locationInfo.EffectivePreferredLocations; /// - /// Returns the location corresponding to the endpoint if location specific endpoint is provided. - /// For the defaultEndPoint, we will return the first available write location. - /// Returns null, in other cases. + /// Returns the region name corresponding to the given endpoint. + /// - If the endpoint matches a known write or read regional endpoint, returns that region name. + /// - If the endpoint is the account's default (global) endpoint and at least one write + /// location is known, returns the first entry of the available write locations list. + /// This applies to both single-master and multi-master accounts. Note that for multi-master + /// accounts the first write location is simply the first region in the configured list, + /// not necessarily the hub/primary write region. + /// - Otherwise, returns null. /// - /// - /// Today we return null for defaultEndPoint if multiple write locations can be used. - /// This needs to be modifed to figure out proper location in such case. - /// public string GetLocation(Uri endpoint) { string location = this.locationInfo.AvailableWriteEndpointByLocation.FirstOrDefault(uri => uri.Value == endpoint).Key ?? this.locationInfo.AvailableReadEndpointByLocation.FirstOrDefault(uri => uri.Value == endpoint).Key; - if (location == null && endpoint == this.defaultEndpoint && !this.CanUseMultipleWriteLocations()) + if (location == null + && endpoint == this.defaultEndpoint + && this.locationInfo.AvailableWriteLocations.Count > 0) { - if (this.locationInfo.AvailableWriteEndpointByLocation.Any()) - { - return this.locationInfo.AvailableWriteEndpointByLocation.First().Key; - } + return this.locationInfo.AvailableWriteLocations[0]; } return location; @@ -189,7 +189,8 @@ public string GetLocation(Uri endpoint) /// /// Set region name for a location if present in the locationcache otherwise set region name as null. - /// If endpoint's hostname is same as default endpoint hostname, set regionName as null. + /// For multi-master accounts, if endpoint's hostname is same as default endpoint hostname, + /// set regionName to the first available write region. /// /// /// @@ -203,12 +204,17 @@ public bool TryGetLocationForGatewayDiagnostics(Uri endpoint, out string regionN UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase) == 0) { - regionName = null; - return false; + // Use account-level enableMultipleWriteLocations (not CanUseMultipleWriteLocations which also + // requires client opt-in) because diagnostics should resolve the region regardless of whether + // the client uses multi-write. The default endpoint routes to the first write region server-side. + regionName = this.enableMultipleWriteLocations + ? this.GetLocation(this.defaultEndpoint) + : null; + return regionName != null; } regionName = this.GetLocation(endpoint); - return true; + return regionName != null; } /// diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/LocationCacheTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/LocationCacheTests.cs index b8f85d4bef..ff46dcee93 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/LocationCacheTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/LocationCacheTests.cs @@ -121,6 +121,95 @@ public void ValidateTryGetLocationForGatewayDiagnostics() } } + [TestMethod] + [Owner("ntripician")] + public void ValidateTryGetLocationForGatewayDiagnosticsOnDefaultEndpointForMultiMaster() + { + using GlobalEndpointManager endpointManager = this.Initialize( + useMultipleWriteLocations: true, + enableEndpointDiscovery: true, + isPreferredLocationsListEmpty: false); + + string expectedRegionName = this.databaseAccount.WriteLocationsInternal.First().Name; + + Assert.AreEqual(expectedRegionName, this.cache.GetLocation(LocationCacheTests.DefaultEndpoint)); + + Assert.AreEqual(true, this.cache.TryGetLocationForGatewayDiagnostics(LocationCacheTests.DefaultEndpoint, out string regionName)); + Assert.AreEqual(expectedRegionName, regionName); + + Assert.AreEqual(true, this.cache.TryGetLocationForGatewayDiagnostics(new Uri(LocationCacheTests.DefaultEndpoint, "random/path"), out regionName)); + Assert.AreEqual(expectedRegionName, regionName); + } + + [TestMethod] + [Owner("ntripician")] + public void ValidateTryGetLocationForGatewayDiagnosticsOnDefaultEndpointForMultiMasterWithClientOptOut() + { + // Account is multi-master but client has UseMultipleWriteLocations = false. + // Diagnostics should still resolve the default endpoint to the first write region. + using GlobalEndpointManager endpointManager = this.Initialize( + useMultipleWriteLocations: false, + enableEndpointDiscovery: true, + isPreferredLocationsListEmpty: false); + + // Override account setting to multi-master (server-side) while client did not opt in + this.databaseAccount = LocationCacheTests.CreateDatabaseAccount( + useMultipleWriteLocations: true, + enforceSingleMasterSingleWriteLocation: false); + this.cache.OnDatabaseAccountRead(this.databaseAccount); + + string expectedRegionName = this.databaseAccount.WriteLocationsInternal.First().Name; + + Assert.AreEqual(expectedRegionName, this.cache.GetLocation(LocationCacheTests.DefaultEndpoint)); + + Assert.AreEqual(true, this.cache.TryGetLocationForGatewayDiagnostics(LocationCacheTests.DefaultEndpoint, out string regionName)); + Assert.AreEqual(expectedRegionName, regionName); + + Assert.AreEqual(true, this.cache.TryGetLocationForGatewayDiagnostics(new Uri(LocationCacheTests.DefaultEndpoint, "random/path"), out regionName)); + Assert.AreEqual(expectedRegionName, regionName); + } + + [TestMethod] + [Owner("ntripician")] + public void ValidateTryGetLocationForGatewayDiagnosticsReturnsFalseForUnknownEndpoint() + { + using GlobalEndpointManager endpointManager = this.Initialize( + useMultipleWriteLocations: true, + enableEndpointDiscovery: true, + isPreferredLocationsListEmpty: false); + + // An endpoint that is neither the default endpoint nor any known regional endpoint + Uri unknownEndpoint = new Uri("https://unknown-region.documents.azure.com"); + + Assert.IsNull(this.cache.GetLocation(unknownEndpoint)); + + Assert.AreEqual(false, this.cache.TryGetLocationForGatewayDiagnostics(unknownEndpoint, out string regionName)); + Assert.IsNull(regionName); + } + + [TestMethod] + [Owner("ntripician")] + public void ValidateTryGetLocationForGatewayDiagnosticsOnDefaultEndpointBeforeAccountRead() + { + // Simulate multimaster cache before any account info is populated. + // AvailableWriteLocations will be empty, so GetLocation should return null. + LocationCache uninitializedCache = new LocationCache( + preferredLocations: new ReadOnlyCollection(new List { "location1" }), + defaultEndpoint: LocationCacheTests.DefaultEndpoint, + enableEndpointDiscovery: true, + connectionLimit: 50, + useMultipleWriteLocations: true); + + // No OnDatabaseAccountRead called, so AvailableWriteLocations is empty + Assert.IsNull(uninitializedCache.GetLocation(LocationCacheTests.DefaultEndpoint)); + + // enableMultipleWriteLocations defaults to false until OnDatabaseAccountRead is called + // with a multi-master account, so TryGetLocationForGatewayDiagnostics falls through to + // the single-master path and returns false + Assert.AreEqual(false, uninitializedCache.TryGetLocationForGatewayDiagnostics(LocationCacheTests.DefaultEndpoint, out string regionName)); + Assert.IsNull(regionName); + } + [TestMethod] [Owner("atulk")] public async Task ValidateRetryOnSessionNotAvailableWithDisableMultipleWriteLocationsAndEndpointDiscoveryDisabled()