diff --git a/Microsoft.Azure.Cosmos/src/DocumentClient.cs b/Microsoft.Azure.Cosmos/src/DocumentClient.cs index 6448d9ea9f..460be822da 100644 --- a/Microsoft.Azure.Cosmos/src/DocumentClient.cs +++ b/Microsoft.Azure.Cosmos/src/DocumentClient.cs @@ -120,6 +120,8 @@ internal partial class DocumentClient : IDisposable, IAuthorizationTokenProvider private readonly bool isReplicaAddressValidationEnabled; private readonly bool enableAsyncCacheExceptionNoSharing; + private readonly bool isThinClientEnabled; + //Fault Injection private readonly IChaosInterceptorFactory chaosInterceptorFactory; private readonly IChaosInterceptor chaosInterceptor; @@ -168,6 +170,7 @@ internal partial class DocumentClient : IDisposable, IAuthorizationTokenProvider private IStoreClientFactory storeClientFactory; internal CosmosHttpClient httpClient { get; private set; } + internal CosmosHttpClient thinClientModeHttpClient { get; private set; } // Flag that indicates whether store client factory must be disposed whenever client is disposed. // Setting this flag to false will result in store client factory not being disposed when client is disposed. // This flag is used to allow shared store client factory survive disposition of a document client while other clients continue using it. @@ -248,6 +251,7 @@ public DocumentClient(Uri serviceEndpoint, cancellationToken: this.cancellationTokenSource.Token, enableAsyncCacheExceptionNoSharing: this.enableAsyncCacheExceptionNoSharing); this.isReplicaAddressValidationEnabled = ConfigurationManager.IsReplicaAddressValidationEnabled(connectionPolicy); + this.isThinClientEnabled = ConfigurationManager.IsThinClientEnabled(defaultValue: false); } /// @@ -505,6 +509,7 @@ internal DocumentClient(Uri serviceEndpoint, enableAsyncCacheExceptionNoSharing: this.enableAsyncCacheExceptionNoSharing); this.chaosInterceptorFactory = chaosInterceptorFactory; this.chaosInterceptor = chaosInterceptorFactory?.CreateInterceptor(this); + this.isThinClientEnabled = ConfigurationManager.IsThinClientEnabled(defaultValue: false); this.Initialize( serviceEndpoint: serviceEndpoint, @@ -516,7 +521,8 @@ internal DocumentClient(Uri serviceEndpoint, storeClientFactory: storeClientFactory, cosmosClientId: cosmosClientId, remoteCertificateValidationCallback: remoteCertificateValidationCallback, - cosmosClientTelemetryOptions: cosmosClientTelemetryOptions); + cosmosClientTelemetryOptions: cosmosClientTelemetryOptions, + enableThinClientMode: this.isThinClientEnabled); } /// @@ -701,7 +707,8 @@ internal virtual void Initialize(Uri serviceEndpoint, TokenCredential tokenCredential = null, string cosmosClientId = null, RemoteCertificateValidationCallback remoteCertificateValidationCallback = null, - CosmosClientTelemetryOptions cosmosClientTelemetryOptions = null) + CosmosClientTelemetryOptions cosmosClientTelemetryOptions = null, + bool enableThinClientMode = false) { if (serviceEndpoint == null) { @@ -967,6 +974,17 @@ internal virtual void Initialize(Uri serviceEndpoint, this.receivedResponse, this.chaosInterceptor); + if (enableThinClientMode) + { + this.thinClientModeHttpClient = CosmosHttpClientCore.CreateWithConnectionPolicy( + this.ApiType, + DocumentClientEventSource.Instance, + this.ConnectionPolicy, + null, + this.sendingRequest, + this.receivedResponse); + } + // Loading VM Information (non blocking call and initialization won't fail if this call fails) VmMetadataApiHandler.TryInitialize(this.httpClient); @@ -1086,6 +1104,21 @@ private async Task GetInitializationTaskAsync(IStoreClientFactory storeCli { this.StoreModel = this.GatewayStoreModel; } + else if (this.isThinClientEnabled) + { + ThinClientStoreModel thinClientStoreModel = new ( + endpointManager: this.GlobalEndpointManager, + this.PartitionKeyRangeLocation, + this.sessionContainer, + (Cosmos.ConsistencyLevel)this.accountServiceConfiguration.DefaultConsistencyLevel, + this.eventSource, + this.serializerSettings, + this.thinClientModeHttpClient); + + thinClientStoreModel.SetCaches(this.partitionKeyRangeCache, this.collectionCache); + + this.StoreModel = thinClientStoreModel; + } else { this.InitializeDirectConnectivity(storeClientFactory); @@ -6567,6 +6600,13 @@ internal IStoreModel GetStoreProxy(DocumentServiceRequest request) return this.GatewayStoreModel; } + if (this.isThinClientEnabled + && operationType == OperationType.Read + && resourceType == ResourceType.Database) + { + return this.GatewayStoreModel; + } + if (operationType == OperationType.Create || operationType == OperationType.Upsert) { @@ -6786,7 +6826,8 @@ private async Task InitializeGatewayConfigurationReaderAsync() cosmosAuthorization: this.cosmosAuthorization, connectionPolicy: this.ConnectionPolicy, httpClient: this.httpClient, - cancellationToken: this.cancellationTokenSource.Token); + cancellationToken: this.cancellationTokenSource.Token, + isThinClientEnabled: this.isThinClientEnabled); this.accountServiceConfiguration = new CosmosAccountServiceConfiguration(accountReader.InitializeReaderAsync); diff --git a/Microsoft.Azure.Cosmos/src/GatewayAccountReader.cs b/Microsoft.Azure.Cosmos/src/GatewayAccountReader.cs index f332d873fd..459ac9c5f5 100644 --- a/Microsoft.Azure.Cosmos/src/GatewayAccountReader.cs +++ b/Microsoft.Azure.Cosmos/src/GatewayAccountReader.cs @@ -17,6 +17,7 @@ namespace Microsoft.Azure.Cosmos internal sealed class GatewayAccountReader { + private readonly bool isThinClientEnabled; private readonly ConnectionPolicy connectionPolicy; private readonly AuthorizationTokenProvider cosmosAuthorization; private readonly CosmosHttpClient httpClient; @@ -28,13 +29,15 @@ public GatewayAccountReader(Uri serviceEndpoint, AuthorizationTokenProvider cosmosAuthorization, ConnectionPolicy connectionPolicy, CosmosHttpClient httpClient, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, + bool isThinClientEnabled = false) { this.httpClient = httpClient; this.serviceEndpoint = serviceEndpoint; this.cosmosAuthorization = cosmosAuthorization ?? throw new ArgumentNullException(nameof(AuthorizationTokenProvider)); this.connectionPolicy = connectionPolicy; this.cancellationToken = cancellationToken; + this.isThinClientEnabled = isThinClientEnabled; } private async Task GetDatabaseAccountAsync(Uri serviceEndpoint) @@ -57,6 +60,13 @@ await this.cosmosAuthorization.AddAuthorizationHeaderAsync( resourceType: ResourceType.DatabaseAccount, authorizationTokenType: AuthorizationTokenType.PrimaryMasterKey)) { + if (this.isThinClientEnabled) + { + headers.Add( + ThinClientConstants.EnableThinClientEndpointDiscoveryHeaderName, + this.isThinClientEnabled.ToString()); + } + using (HttpResponseMessage responseMessage = await this.httpClient.GetAsync( uri: serviceEndpoint, additionalHeaders: headers, diff --git a/Microsoft.Azure.Cosmos/src/Resource/Settings/AccountProperties.cs b/Microsoft.Azure.Cosmos/src/Resource/Settings/AccountProperties.cs index 7092e60d88..cfc42707fb 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Settings/AccountProperties.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Settings/AccountProperties.cs @@ -28,6 +28,9 @@ public class AccountProperties /// internal AccountProperties() { + this.ThinClientWritableLocationsInternal = new Collection(); + this.ThinClientReadableLocationsInternal = new Collection(); + this.QueryEngineConfigurationInternal = new Lazy>(() => this.QueryStringToDictConverter()); } @@ -127,6 +130,18 @@ internal Collection ReadLocationsInternal set => this.readRegions = value; } + /// + /// Gets or sets the set of ThinClient writable locations parsed from AdditionalProperties. + /// + [JsonIgnore] + internal Collection ThinClientWritableLocationsInternal { get; set; } + + /// + /// Gets or sets the set of ThinClient readable locations parsed from AdditionalProperties. + /// + [JsonIgnore] + internal Collection ThinClientReadableLocationsInternal { get; set; } + /// /// Gets the storage quota for media storage in the databaseAccount from the Azure Cosmos DB service. /// @@ -241,16 +256,12 @@ private IDictionary QueryStringToDictConverter() } } - /// - /// This contains the thinclient endpoint value. - /// - internal Uri ThinClientEndpoint { get; set; } - /// /// This contains additional values for scenarios where the SDK is not aware of new fields. /// This ensures that if resource is read and updated none of the fields will be lost in the process. /// [JsonExtensionData] - internal IDictionary AdditionalProperties { get; private set; } + internal IDictionary AdditionalProperties { get; set; } + } } diff --git a/Microsoft.Azure.Cosmos/src/Routing/GlobalEndpointManager.cs b/Microsoft.Azure.Cosmos/src/Routing/GlobalEndpointManager.cs index ce2e7ca535..4d7884ec58 100644 --- a/Microsoft.Azure.Cosmos/src/Routing/GlobalEndpointManager.cs +++ b/Microsoft.Azure.Cosmos/src/Routing/GlobalEndpointManager.cs @@ -15,7 +15,8 @@ namespace Microsoft.Azure.Cosmos.Routing using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Common; using Microsoft.Azure.Cosmos.Core.Trace; - using Microsoft.Azure.Documents; + using Microsoft.Azure.Documents; + using Newtonsoft.Json.Linq; /// /// AddressCache implementation for client SDK. Supports cross region address routing based on @@ -499,6 +500,52 @@ public void Dispose() // that is never awaited on so it will not be thrown back to the caller. this.cancellationTokenSource.Dispose(); } + } + + /// + /// Parse thinClientWritableLocations / thinClientReadableLocations from AdditionalProperties. + /// + private static void ParseThinClientLocationsFromAdditionalProperties(AccountProperties databaseAccount) + { + if (databaseAccount?.AdditionalProperties != null) + { + if (databaseAccount.AdditionalProperties.TryGetValue("thinClientWritableLocations", out JToken writableToken) + && writableToken is JArray writableArray) + { + databaseAccount.ThinClientWritableLocationsInternal = ParseAccountRegionArray(writableArray); + } + + if (databaseAccount.AdditionalProperties.TryGetValue("thinClientReadableLocations", out JToken readableToken) + && readableToken is JArray readableArray) + { + databaseAccount.ThinClientReadableLocationsInternal = ParseAccountRegionArray(readableArray); + } + } + } + + private static Collection ParseAccountRegionArray(JArray array) + { + Collection result = new Collection(); + foreach (JToken token in array) + { + if (token is not JObject obj) + { + continue; + } + + string? regionName = obj["name"]?.ToString(); + string? endpointStr = obj["databaseAccountEndpoint"]?.ToString(); + + if (!string.IsNullOrEmpty(regionName) && !string.IsNullOrEmpty(endpointStr)) + { + result.Add(new AccountRegion + { + Name = regionName, + Endpoint = endpointStr + }); + } + } + return result; } public virtual void InitializeAccountPropertiesAndStartBackgroundRefresh(AccountProperties databaseAccount) @@ -662,6 +709,8 @@ private async Task RefreshDatabaseAccountInternalAsync(bool forceRefresh) { this.LastBackgroundRefreshUtc = DateTime.UtcNow; AccountProperties accountProperties = await this.GetDatabaseAccountAsync(true); + + GlobalEndpointManager.ParseThinClientLocationsFromAdditionalProperties(accountProperties); this.locationCache.OnDatabaseAccountRead(accountProperties); @@ -717,6 +766,15 @@ public IList GetEffectivePreferredLocations() return this.connectionPolicy.PreferredLocations?.Count > 0 ? this.connectionPolicy.PreferredLocations : this.locationCache.EffectivePreferredLocations; + } + + public Uri ResolveThinClientEndpoint(DocumentServiceRequest request) + { + bool isReadRequest = request.IsReadOnlyRequest + || request.OperationType == OperationType.Query + || request.OperationType == OperationType.ReadFeed; + + return this.locationCache.ResolveThinClientEndpoint(request, isReadRequest); } } } diff --git a/Microsoft.Azure.Cosmos/src/Routing/LocationCache.cs b/Microsoft.Azure.Cosmos/src/Routing/LocationCache.cs index 4924bb2971..317b563ab2 100644 --- a/Microsoft.Azure.Cosmos/src/Routing/LocationCache.cs +++ b/Microsoft.Azure.Cosmos/src/Routing/LocationCache.cs @@ -199,7 +199,9 @@ public void OnDatabaseAccountRead(AccountProperties databaseAccount) { this.UpdateLocationCache( databaseAccount.WritableRegions, - databaseAccount.ReadableRegions, + databaseAccount.ReadableRegions, + thinClientWriteLocations: databaseAccount.ThinClientWritableLocationsInternal, + thinClientReadLocations: databaseAccount.ThinClientReadableLocationsInternal, preferenceList: null, enableMultipleWriteLocations: databaseAccount.EnableMultipleWriteLocations); } @@ -638,12 +640,14 @@ private void MarkEndpointUnavailable( unavailableEndpoint, unavailableOperationType, updatedInfo.LastUnavailabilityCheckTimeStamp); - } - - private void UpdateLocationCache( - IEnumerable writeLocations = null, - IEnumerable readLocations = null, - ReadOnlyCollection preferenceList = null, + } + + private void UpdateLocationCache( + IEnumerable writeLocations = null, + IEnumerable readLocations = null, + IEnumerable thinClientWriteLocations = null, + IEnumerable thinClientReadLocations = null, + ReadOnlyCollection preferenceList = null, bool? enableMultipleWriteLocations = null) { lock (this.lockObject) @@ -683,6 +687,28 @@ private void UpdateLocationCache( nextLocationInfo.AvailableWriteLocations = availableWriteLocations; nextLocationInfo.AvailableWriteLocationByEndpoint = availableWriteLocationsByEndpoint; + } + + if (thinClientReadLocations != null) + { + nextLocationInfo.ThinClientReadEndpointByLocation = this.GetEndpointByLocation( + thinClientReadLocations, + out ReadOnlyCollection tcreadLocations, + out ReadOnlyDictionary tcReadLocByEndpoint); + + nextLocationInfo.ThinClientReadLocations = tcreadLocations; + nextLocationInfo.ThinClientReadLocationByEndpoint = tcReadLocByEndpoint; + } + + if (thinClientWriteLocations != null) + { + nextLocationInfo.ThinClientWriteEndpointByLocation = this.GetEndpointByLocation( + thinClientWriteLocations, + out ReadOnlyCollection tcwriteLocations, + out ReadOnlyDictionary tcWriteLocByEndpoint); + + nextLocationInfo.ThinClientWriteLocations = tcwriteLocations; + nextLocationInfo.ThinClientWriteLocationByEndpoint = tcWriteLocByEndpoint; } nextLocationInfo.WriteEndpoints = this.GetPreferredAvailableEndpoints( @@ -697,7 +723,19 @@ private void UpdateLocationCache( expectedAvailableOperation: OperationType.Read, fallbackEndpoint: nextLocationInfo.WriteEndpoints[0]); - nextLocationInfo.EffectivePreferredLocations = nextLocationInfo.PreferredLocations; + nextLocationInfo.EffectivePreferredLocations = nextLocationInfo.PreferredLocations; + + nextLocationInfo.ThinClientReadEndpoints = this.GetPreferredAvailableEndpoints( + endpointsByLocation: nextLocationInfo.ThinClientReadEndpointByLocation, + orderedLocations: nextLocationInfo.ThinClientReadLocations, + expectedAvailableOperation: OperationType.Read, + fallbackEndpoint: this.defaultEndpoint); + + nextLocationInfo.ThinClientWriteEndpoints = this.GetPreferredAvailableEndpoints( + endpointsByLocation: nextLocationInfo.ThinClientWriteEndpointByLocation, + orderedLocations: nextLocationInfo.ThinClientWriteLocations, + expectedAvailableOperation: OperationType.Write, + fallbackEndpoint: this.defaultEndpoint); if (nextLocationInfo.PreferredLocations == null || nextLocationInfo.PreferredLocations.Count == 0) { @@ -855,6 +893,20 @@ internal bool CanUseMultipleWriteLocations() { return this.useMultipleWriteLocations && this.enableMultipleWriteLocations; } + + internal Uri ResolveThinClientEndpoint(DocumentServiceRequest request, bool isReadRequest) + { + DatabaseAccountLocationsInfo snapshot = this.locationInfo; + ReadOnlyCollection endpoints = isReadRequest + ? snapshot.ThinClientReadEndpoints + : snapshot.ThinClientWriteEndpoints; + + int locationIndex = request.RequestContext.LocationIndexToRoute.GetValueOrDefault(0); + Uri chosenEndpoint = endpoints[locationIndex % endpoints.Count]; + + request.RequestContext.RouteToLocation(chosenEndpoint); + return chosenEndpoint; + } private void SetServicePointConnectionLimit(Uri endpoint) { @@ -887,7 +939,21 @@ public DatabaseAccountLocationsInfo(ReadOnlyCollection preferredLocation this.WriteEndpoints = new List() { defaultEndpoint }.AsReadOnly(); this.AccountReadEndpoints = new List() { defaultEndpoint }.AsReadOnly(); this.ReadEndpoints = new List() { defaultEndpoint }.AsReadOnly(); - this.EffectivePreferredLocations = new List().AsReadOnly(); + this.EffectivePreferredLocations = new List().AsReadOnly(); + + this.ThinClientWriteLocations = new List().AsReadOnly(); + this.ThinClientReadLocations = new List().AsReadOnly(); + this.ThinClientWriteEndpointByLocation = + new ReadOnlyDictionary(new Dictionary()); + this.ThinClientReadEndpointByLocation = + new ReadOnlyDictionary(new Dictionary()); + this.ThinClientWriteLocationByEndpoint = + new ReadOnlyDictionary(new Dictionary()); + this.ThinClientReadLocationByEndpoint = + new ReadOnlyDictionary(new Dictionary()); + this.ThinClientWriteEndpoints = new List() { defaultEndpoint }.AsReadOnly(); + this.ThinClientReadEndpoints = new List() { defaultEndpoint }.AsReadOnly(); + } public DatabaseAccountLocationsInfo(DatabaseAccountLocationsInfo other) @@ -902,7 +968,16 @@ public DatabaseAccountLocationsInfo(DatabaseAccountLocationsInfo other) this.WriteEndpoints = other.WriteEndpoints; this.AccountReadEndpoints = other.AccountReadEndpoints; this.ReadEndpoints = other.ReadEndpoints; - this.EffectivePreferredLocations = other.EffectivePreferredLocations; + this.EffectivePreferredLocations = other.EffectivePreferredLocations; + + this.ThinClientWriteLocations = other.ThinClientWriteLocations; + this.ThinClientReadLocations = other.ThinClientReadLocations; + this.ThinClientWriteEndpointByLocation = other.ThinClientWriteEndpointByLocation; + this.ThinClientReadEndpointByLocation = other.ThinClientReadEndpointByLocation; + this.ThinClientWriteLocationByEndpoint = other.ThinClientWriteLocationByEndpoint; + this.ThinClientReadLocationByEndpoint = other.ThinClientReadLocationByEndpoint; + this.ThinClientWriteEndpoints = other.ThinClientWriteEndpoints; + this.ThinClientReadEndpoints = other.ThinClientReadEndpoints; } public ReadOnlyCollection PreferredLocations { get; set; } @@ -916,7 +991,17 @@ public DatabaseAccountLocationsInfo(DatabaseAccountLocationsInfo other) public ReadOnlyCollection WriteEndpoints { get; set; } public ReadOnlyCollection ReadEndpoints { get; set; } public ReadOnlyCollection AccountReadEndpoints { get; set; } - public ReadOnlyCollection EffectivePreferredLocations { get; set; } + public ReadOnlyCollection EffectivePreferredLocations { get; set; } + public ReadOnlyCollection ThinClientWriteLocations { get; set; } + public ReadOnlyDictionary ThinClientWriteEndpointByLocation { get; set; } + public ReadOnlyDictionary ThinClientWriteLocationByEndpoint { get; set; } + public ReadOnlyCollection ThinClientWriteEndpoints { get; set; } + + public ReadOnlyCollection ThinClientReadLocations { get; set; } + public ReadOnlyDictionary ThinClientReadEndpointByLocation { get; set; } + public ReadOnlyDictionary ThinClientReadLocationByEndpoint { get; set; } + public ReadOnlyCollection ThinClientReadEndpoints { get; set; } + } [Flags] diff --git a/Microsoft.Azure.Cosmos/src/ThinClientConstants.cs b/Microsoft.Azure.Cosmos/src/ThinClientConstants.cs index 37dcd5ce37..9a86df2922 100644 --- a/Microsoft.Azure.Cosmos/src/ThinClientConstants.cs +++ b/Microsoft.Azure.Cosmos/src/ThinClientConstants.cs @@ -16,5 +16,6 @@ internal static class ThinClientConstants public const string ProxyOperationType = "x-ms-thinclient-proxy-operation-type"; public const string ProxyResourceType = "x-ms-thinclient-proxy-resource-type"; public const string EffectivePartitionKey = "x-ms-effective-partition-key"; + public const string EnableThinClientEndpointDiscoveryHeaderName = "x-ms-cosmos-use-thinclient"; } } diff --git a/Microsoft.Azure.Cosmos/src/ThinClientStoreModel.cs b/Microsoft.Azure.Cosmos/src/ThinClientStoreModel.cs index fc8a3ef4dc..2aa8f3df21 100644 --- a/Microsoft.Azure.Cosmos/src/ThinClientStoreModel.cs +++ b/Microsoft.Azure.Cosmos/src/ThinClientStoreModel.cs @@ -66,12 +66,12 @@ await GatewayStoreModel.ApplySessionTokenAsync( request.RequestContext.RegionName = regionName; } - AccountProperties properties = await this.GetDatabaseAccountSafeAsync(); + AccountProperties properties = await this.GetDatabaseAccountPropertiesAsync(); response = await this.thinClientStoreClient.InvokeAsync( request, request.ResourceType, physicalAddress, - properties.ThinClientEndpoint, + this.endpointManager.ResolveThinClientEndpoint(request), properties.Id, base.clientCollectionCache, cancellationToken); @@ -101,7 +101,7 @@ await this.CaptureSessionTokenAndHandleSplitAsync( return response; } - private async Task GetDatabaseAccountSafeAsync() + private async Task GetDatabaseAccountPropertiesAsync() { try { diff --git a/Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs b/Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs index b3bd045c0c..5cf41f4449 100644 --- a/Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs +++ b/Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs @@ -40,6 +40,11 @@ internal static class ConfigurationManager /// internal static readonly string AllowedPartitionUnavailabilityDurationInSeconds = "AZURE_COSMOS_PPCB_ALLOWED_PARTITION_UNAVAILABILITY_DURATION_IN_SECONDS"; + /// + /// Environment variable name to enable thin client mode. + /// + internal static readonly string ThinClientModeEnabled = "AZURE_COSMOS_THIN_CLIENT_ENABLED"; + /// /// A read-only string containing the environment variable name for capturing the consecutive failure count for reads, before triggering per partition /// circuit breaker flow. The default value for this interval is 10 consecutive requests within 1 min window. @@ -132,6 +137,20 @@ public static bool IsPartitionLevelFailoverEnabled( defaultValue: defaultValue); } + /// + /// Gets the boolean value indicating whether the thin client mode is enabled based on the environment variable override. + /// + /// A boolean field containing the default value for thin client mode. + /// A boolean flag indicating if thin client mode is enabled. + public static bool IsThinClientEnabled( + bool defaultValue) + { + return ConfigurationManager + .GetEnvironmentVariable( + variable: ConfigurationManager.ThinClientModeEnabled, + defaultValue: defaultValue); + } + /// /// Gets the boolean value of the partition level circuit breaker environment variable. Note that, partition level /// circuit breaker is disabled by default for both preview and GA releases. The user can set the respective diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GlobalEndpointManagerTest.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GlobalEndpointManagerTest.cs index 2299bb9a27..3e9b22bd83 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GlobalEndpointManagerTest.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GlobalEndpointManagerTest.cs @@ -16,6 +16,7 @@ namespace Microsoft.Azure.Cosmos using Microsoft.Azure.Documents; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; + using Newtonsoft.Json.Linq; /// /// Tests for @@ -684,6 +685,89 @@ void TraceHandler(string message) Environment.SetEnvironmentVariable("MinimumIntervalForNonForceRefreshLocationInMS", originalConfigValue); } + [TestMethod] + public async Task ThinClientEndpoints_ParsesAndResolves() + { + // Arrange + Collection readableLocations = new Collection + { + new AccountRegion { Name = "ReadLocation", Endpoint = "https://readlocation.documents.azure.com" } + }; + Collection writeableLocations = new Collection + { + new AccountRegion { Name = "WriteLocation", Endpoint = "https://writelocation.documents.azure.com" } + }; + + AccountProperties accountProperties = new AccountProperties + { + ReadLocationsInternal = readableLocations, + WriteLocationsInternal = writeableLocations, + AdditionalProperties = new Dictionary + { + { + "thinClientWritableLocations", + JArray.Parse(@"[ + { 'name': 'ThinClientRegionWrite', 'databaseAccountEndpoint': 'https://thinclientwrite.documents.azure.com:10650/' } + ]") + }, + { + "thinClientReadableLocations", + JArray.Parse(@"[ + { 'name': 'ThinClientRegionRead', 'databaseAccountEndpoint': 'https://thinclientread.documents.azure.com:10650/' } + ]") + } + } + }; + + Mock mockOwner = new Mock(); + mockOwner.Setup(owner => owner.ServiceEndpoint).Returns(new Uri("https://defaultendpoint.net/")); + + // Returning updated accountProperties + mockOwner.Setup(owner => owner.GetDatabaseAccountInternalAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(accountProperties); + + ConnectionPolicy connectionPolicy = new ConnectionPolicy + { + EnableEndpointDiscovery = true, + UseMultipleWriteLocations = false, + }; + + GlobalEndpointManager gem = new GlobalEndpointManager(mockOwner.Object, connectionPolicy); + try + { + // Act: Initialize once + gem.InitializeAccountPropertiesAndStartBackgroundRefresh(accountProperties); + + // Forcibly refresh + await gem.RefreshLocationAsync(forceRefresh: true); + + // Create a test DocumentServiceRequest that is read + DocumentServiceRequest readRequest = DocumentServiceRequest.Create( + OperationType.Read, + ResourceType.Document, + AuthorizationTokenType.PrimaryMasterKey); + + Uri thinClientReadEndpoint = gem.ResolveThinClientEndpoint(readRequest); + + // Create a test DocumentServiceRequest that is write + DocumentServiceRequest writeRequest = DocumentServiceRequest.Create( + OperationType.Create, + ResourceType.Document, + AuthorizationTokenType.PrimaryMasterKey); + + Uri thinClientWriteEndpoint = gem.ResolveThinClientEndpoint(writeRequest); + + // Assert: + Assert.AreEqual("https://thinclientread.documents.azure.com:10650/", thinClientReadEndpoint.AbsoluteUri); + + Assert.AreEqual("https://thinclientwrite.documents.azure.com:10650/", thinClientWriteEndpoint.AbsoluteUri); + } + finally + { + gem.Dispose(); + } + } + private class TestTraceListener : TraceListener { public Action Callback { get; set; } 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 4dff19d779..52e36c2908 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/LocationCacheTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/LocationCacheTests.cs @@ -1433,6 +1433,77 @@ public void VerifyRegionExcludedTest( } + [TestMethod] + public void ValidateThinClientLocationCacheFlowTest() + { + // Arrange: + Collection normalReads = new Collection() + { + new AccountRegion { Name = "ReadLocation", Endpoint = "https://readlocation.documents.azure.com" } + }; + + Collection normalWrites = new Collection() + { + new AccountRegion { Name = "WriteLocation", Endpoint = "https://writelocation.documents.azure.com" } + }; + + Collection thinClientReads = new Collection() + { + new AccountRegion { Name = "ThinClientReadLocation", Endpoint = "https://thinclient-read.documents.azure.com:10650/" } + }; + + Collection thinClientWrites = new Collection() + { + new AccountRegion { Name = "ThinClientWriteLocation", Endpoint = "https://thinclient-write.documents.azure.com:10650/" } + }; + + AccountProperties accountProps = new AccountProperties + { + ReadLocationsInternal = normalReads, + WriteLocationsInternal = normalWrites, + ThinClientReadableLocationsInternal = thinClientReads, + ThinClientWritableLocationsInternal = thinClientWrites, + EnableMultipleWriteLocations = false + }; + + LocationCache cache = new LocationCache( + preferredLocations: new ReadOnlyCollection(new List()), + defaultEndpoint: new Uri("https://defaultendpoint.documents.azure.com"), + enableEndpointDiscovery: true, + connectionLimit: 50, + useMultipleWriteLocations: false); + + // Act: + cache.OnDatabaseAccountRead(accountProps); + + // Create a read request + DocumentServiceRequest readRequest = DocumentServiceRequest.Create( + OperationType.Read, + ResourceType.Document, + AuthorizationTokenType.PrimaryMasterKey); + + Uri resolvedThinRead = cache.ResolveThinClientEndpoint(readRequest, isReadRequest: true); + + // Create a write request + DocumentServiceRequest writeRequest = DocumentServiceRequest.Create( + OperationType.Create, + ResourceType.Document, + AuthorizationTokenType.PrimaryMasterKey); + + Uri resolvedThinWrite = cache.ResolveThinClientEndpoint(writeRequest, isReadRequest: false); + + // Assert: + Assert.AreEqual("https://thinclient-read.documents.azure.com:10650/", resolvedThinRead.AbsoluteUri, + "ThinClient read endpoint must match the one we provided in ThinClientReadableLocationsInternal"); + + Assert.AreEqual("https://thinclient-write.documents.azure.com:10650/", resolvedThinWrite.AbsoluteUri, + "ThinClient write endpoint must match the one we provided in ThinClientWritableLocationsInternal"); + + Assert.AreEqual("https://readlocation.documents.azure.com/", cache.ReadEndpoints[0].AbsoluteUri); + Assert.AreEqual("https://writelocation.documents.azure.com/", cache.WriteEndpoints[0].AbsoluteUri); + } + + private ReadOnlyCollection GetApplicableRegions(bool isReadRequest, bool useMultipleWriteLocations, bool usesPreferredLocations, List excludeRegions, bool isDefaultEndpointARegionalEndpoint) { // exclusion of write region for single-write maps to first available write region diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ThinClientStoreModelTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ThinClientStoreModelTests.cs index fcbb7baa2d..0b86fbd583 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ThinClientStoreModelTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ThinClientStoreModelTests.cs @@ -101,10 +101,7 @@ public async Task ProcessMessageAsync_Success_ShouldReturnDocumentServiceRespons Mock docClientMulti = new Mock(); docClientMulti.Setup(c => c.ServiceEndpoint).Returns(new Uri("http://localhost")); - AccountProperties validAccountProperties = new AccountProperties - { - ThinClientEndpoint = new Uri("http://localhost/thinClient/") - }; + AccountProperties validAccountProperties = new AccountProperties(); docClientMulti .Setup(c => c.GetDatabaseAccountInternalAsync(It.IsAny(), It.IsAny())) @@ -197,10 +194,7 @@ public async Task ProcessMessageAsync_404_ShouldThrowDocumentClientException() .Setup(c => c.ServiceEndpoint) .Returns(new Uri("https://myCosmosAccount.documents.azure.com/")); - AccountProperties validProperties = new AccountProperties - { - ThinClientEndpoint = new Uri("https://myThinClientEndpoint/") - }; + AccountProperties validProperties = new AccountProperties(); docClientOkay .Setup(c => c.GetDatabaseAccountInternalAsync(It.IsAny(), It.IsAny()))