Skip to content

Commit

Permalink
Transport: Add HttpClientFactory support (#1441)
Browse files Browse the repository at this point in the history
* contract

* uts

* more tests

* Emulator tests

* contract

* CTOR refactor

* contract

* features

* tests
  • Loading branch information
ealsur authored May 1, 2020
1 parent 01d95a7 commit 5024c15
Show file tree
Hide file tree
Showing 11 changed files with 317 additions and 16 deletions.
10 changes: 10 additions & 0 deletions Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Microsoft.Azure.Cosmos
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Net.Http;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;

Expand Down Expand Up @@ -423,6 +424,15 @@ public PortReuseMode? PortReuseMode
set;
}

/// <summary>
/// Gets or sets a delegate to use to obtain an HttpClient instance to be used for HTTPS communication.
/// </summary>
public Func<HttpClient> HttpClientFactory
{
get;
set;
}

/// <summary>
/// (Direct/TCP) This is an advanced setting that controls the number of TCP connections that will be opened eagerly to each Cosmos DB back-end.
/// </summary>
Expand Down
41 changes: 40 additions & 1 deletion Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Microsoft.Azure.Cosmos
using System.Data.Common;
using System.Linq;
using System.Net;
using System.Net.Http;
using Microsoft.Azure.Cosmos.Fluent;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
Expand Down Expand Up @@ -66,6 +67,7 @@ public class CosmosClientOptions
private int? maxTcpConnectionsPerEndpoint;
private PortReuseMode? portReuseMode;
private IWebProxy webProxy;
private Func<HttpClient> httpClientFactory;

/// <summary>
/// Creates a new CosmosClientOptions
Expand Down Expand Up @@ -321,6 +323,11 @@ public IWebProxy WebProxy
{
throw new ArgumentException($"{nameof(this.WebProxy)} requires {nameof(this.ConnectionMode)} to be set to {nameof(ConnectionMode.Gateway)}");
}

if (this.HttpClientFactory != null)
{
throw new ArgumentException($"{nameof(this.WebProxy)} cannot be set along {nameof(this.HttpClientFactory)}");
}
}
}

Expand Down Expand Up @@ -426,6 +433,32 @@ public CosmosSerializer Serializer
/// </value>
public bool EnableTcpConnectionEndpointRediscovery { get; set; } = false;

/// <summary>
/// Gets or sets a delegate to use to obtain an HttpClient instance to be used for HTTPS communication.
/// </summary>
/// <remarks>
/// <para>
/// HTTPS communication is used when <see cref="ConnectionMode"/> is set to <see cref="ConnectionMode.Gateway"/> for all operations and when <see cref="ConnectionMode"/> is <see cref="ConnectionMode.Direct"/> (default) for metadata operations.
/// </para>
/// <para>
/// Useful in scenarios where the application is using a pool of HttpClient instances to be shared, like ASP.NET Core applications with IHttpClientFactory or Blazor WebAssembly applications.
/// </para>
/// </remarks>
[JsonIgnore]
public Func<HttpClient> HttpClientFactory
{
get => this.httpClientFactory;
set
{
if (this.WebProxy != null)
{
throw new ArgumentException($"{nameof(this.HttpClientFactory)} cannot be set along {nameof(this.WebProxy)}");
}

this.httpClientFactory = value;
}
}

/// <summary>
/// Gets or sets the connection protocol when connecting to the Azure Cosmos service.
/// </summary>
Expand Down Expand Up @@ -556,7 +589,8 @@ internal ConnectionPolicy GetConnectionPolicy()
MaxTcpConnectionsPerEndpoint = this.MaxTcpConnectionsPerEndpoint,
EnableEndpointDiscovery = !this.LimitToEndpoint,
PortReuseMode = this.portReuseMode,
EnableTcpConnectionEndpointRediscovery = this.EnableTcpConnectionEndpointRediscovery
EnableTcpConnectionEndpointRediscovery = this.EnableTcpConnectionEndpointRediscovery,
HttpClientFactory = this.httpClientFactory
};

if (this.ApplicationRegion != null)
Expand Down Expand Up @@ -737,6 +771,11 @@ private string GetUserAgentFeatures()
features |= CosmosClientOptionsFeatures.AllowBulkExecution;
}

if (this.HttpClientFactory != null)
{
features |= CosmosClientOptionsFeatures.HttpClientFactory;
}

if (features == CosmosClientOptionsFeatures.NoFeatures)
{
return null;
Expand Down
3 changes: 2 additions & 1 deletion Microsoft.Azure.Cosmos/src/CosmosClientOptionsFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Microsoft.Azure.Cosmos
internal enum CosmosClientOptionsFeatures
{
NoFeatures = 0,
AllowBulkExecution = 1
AllowBulkExecution = 1,
HttpClientFactory = 2
}
}
19 changes: 18 additions & 1 deletion Microsoft.Azure.Cosmos/src/DocumentClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,23 @@ private async Task GetInitializationTaskAsync(IStoreClientFactory storeClientFac
this.EnsureValidOverwrite(this.desiredConsistencyLevel.Value);
}

GatewayStoreModel gatewayStoreModel = new GatewayStoreModel(
GatewayStoreModel gatewayStoreModel;
if (this.ConnectionPolicy.HttpClientFactory != null)
{
gatewayStoreModel = new GatewayStoreModel(
this.GlobalEndpointManager,
this.sessionContainer,
this.ConnectionPolicy.RequestTimeout,
(Cosmos.ConsistencyLevel)this.accountServiceConfiguration.DefaultConsistencyLevel,
this.eventSource,
this.serializerSettings,
this.ConnectionPolicy.UserAgentContainer,
this.ApiType,
this.ConnectionPolicy.HttpClientFactory);
}
else
{
gatewayStoreModel = new GatewayStoreModel(
this.GlobalEndpointManager,
this.sessionContainer,
this.ConnectionPolicy.RequestTimeout,
Expand All @@ -1127,6 +1143,7 @@ private async Task GetInitializationTaskAsync(IStoreClientFactory storeClientFac
this.ConnectionPolicy.UserAgentContainer,
this.ApiType,
this.httpMessageHandler);
}

this.GatewayStoreModel = gatewayStoreModel;

Expand Down
21 changes: 21 additions & 0 deletions Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Microsoft.Azure.Cosmos.Fluent
{
using System;
using System.Net;
using System.Net.Http;
using Microsoft.Azure.Cosmos.Core.Trace;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
Expand Down Expand Up @@ -391,6 +392,26 @@ public CosmosClientBuilder WithBulkExecution(bool enabled)
return this;
}

/// <summary>
/// Sets a delegate to use to obtain an HttpClient instance to be used for HTTPS communication.
/// </summary>
/// <param name="httpClientFactory">A delegate function to generate instances of HttpClient.</param>
/// <remarks>
/// <para>
/// HTTPS communication is used when <see cref="ConnectionMode"/> is set to <see cref="ConnectionMode.Gateway"/> for all operations and when <see cref="ConnectionMode"/> is <see cref="ConnectionMode.Direct"/> (default) for metadata operations.
/// </para>
/// <para>
/// Useful in scenarios where the application is using a pool of HttpClient instances to be shared, like ASP.NET Core applications with IHttpClientFactory or Blazor WebAssembly applications.
/// </para>
/// </remarks>
/// <returns>The <see cref="CosmosClientBuilder"/> object</returns>
/// <seealso cref="CosmosClientOptions.HttpClientFactory"/>
public CosmosClientBuilder WithHttpClientFactory(Func<HttpClient> httpClientFactory)
{
this.clientOptions.HttpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
return this;
}

/// <summary>
/// Provider that allows encrypting and decrypting data.
/// See https://aka.ms/CosmosClientEncryption for more information on client-side encryption support in Azure Cosmos DB.
Expand Down
74 changes: 64 additions & 10 deletions Microsoft.Azure.Cosmos/src/GatewayStoreModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,80 @@ internal class GatewayStoreModel : IStoreModel, IDisposable
private GatewayStoreClient gatewayStoreClient;
private CookieContainer cookieJar;

public GatewayStoreModel(
private GatewayStoreModel(
GlobalEndpointManager endpointManager,
ISessionContainer sessionContainer,
TimeSpan requestTimeout,
ConsistencyLevel defaultConsistencyLevel,
DocumentClientEventSource eventSource,
JsonSerializerSettings serializerSettings,
UserAgentContainer userAgent,
ApiType apiType = ApiType.None,
HttpMessageHandler messageHandler = null)
DocumentClientEventSource eventSource)
{
// CookieContainer is not really required, but is helpful in debugging.
this.cookieJar = new CookieContainer();
this.endpointManager = endpointManager;
HttpClient httpClient = new HttpClient(messageHandler ?? new HttpClientHandler { CookieContainer = this.cookieJar });
this.sessionContainer = sessionContainer;
this.defaultConsistencyLevel = defaultConsistencyLevel;
this.eventSource = eventSource;
}

public GatewayStoreModel(
GlobalEndpointManager endpointManager,
ISessionContainer sessionContainer,
TimeSpan requestTimeout,
ConsistencyLevel defaultConsistencyLevel,
DocumentClientEventSource eventSource,
JsonSerializerSettings serializerSettings,
UserAgentContainer userAgent,
ApiType apiType,
HttpMessageHandler messageHandler)
: this(endpointManager,
sessionContainer,
defaultConsistencyLevel,
eventSource)
{
this.InitializeGatewayStoreClient(
requestTimeout,
serializerSettings,
userAgent,
apiType,
new HttpClient(messageHandler ?? new HttpClientHandler { CookieContainer = this.cookieJar }));
}

public GatewayStoreModel(
GlobalEndpointManager endpointManager,
ISessionContainer sessionContainer,
TimeSpan requestTimeout,
ConsistencyLevel defaultConsistencyLevel,
DocumentClientEventSource eventSource,
JsonSerializerSettings serializerSettings,
UserAgentContainer userAgent,
ApiType apiType,
Func<HttpClient> httpClientFactory)
: this(endpointManager,
sessionContainer,
defaultConsistencyLevel,
eventSource)
{
HttpClient httpClient = httpClientFactory();
if (httpClient == null)
{
throw new InvalidOperationException("HttpClientFactory did not produce an HttpClient");
}

this.InitializeGatewayStoreClient(
requestTimeout,
serializerSettings,
userAgent,
apiType,
httpClient);

}

private void InitializeGatewayStoreClient(
TimeSpan requestTimeout,
JsonSerializerSettings serializerSettings,
UserAgentContainer userAgent,
ApiType apiType,
HttpClient httpClient)
{
// Use max of client specified and our own request timeout value when sending
// requests to gateway. Otherwise, we will have gateway's transient
// error hiding retries are of no use.
Expand All @@ -65,12 +121,10 @@ public GatewayStoreModel(

httpClient.DefaultRequestHeaders.Add(HttpConstants.HttpHeaders.Accept, RuntimeConstants.MediaTypes.Json);

this.eventSource = eventSource;
this.gatewayStoreClient = new GatewayStoreClient(
httpClient,
this.eventSource,
serializerSettings);

}

public virtual async Task<DocumentServiceResponse> ProcessMessageAsync(DocumentServiceRequest request, CancellationToken cancellationToken = default(CancellationToken))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

Expand Down Expand Up @@ -301,6 +302,45 @@ await Assert.ThrowsExceptionAsync<HttpRequestException>(async () => {
DatabaseResponse databaseResponse = await cosmosClient.CreateDatabaseAsync(Guid.NewGuid().ToString());
});
}

[TestMethod]
public async Task HttpClientFactorySmokeTest()
{
HttpClient client = new HttpClient();
Mock<Func<HttpClient>> factory = new Mock<Func<HttpClient>>();
factory.Setup(f => f()).Returns(client);
CosmosClient cosmosClient = new CosmosClient(
ConfigurationManager.AppSettings["GatewayEndpoint"],
ConfigurationManager.AppSettings["MasterKey"],
new CosmosClientOptions
{
ApplicationName = "test",
ConnectionMode = ConnectionMode.Gateway,
ConnectionProtocol = Protocol.Https,
HttpClientFactory = factory.Object
}
);

string someId = Guid.NewGuid().ToString();
Cosmos.Database database = null;
try
{
database = await cosmosClient.CreateDatabaseAsync(someId);
Cosmos.Container container = await database.CreateContainerAsync(Guid.NewGuid().ToString(), "/id");
await container.CreateItemAsync<dynamic>(new { id = someId });
await container.ReadItemAsync<dynamic>(someId, new Cosmos.PartitionKey(someId));
await container.DeleteItemAsync<dynamic>(someId, new Cosmos.PartitionKey(someId));
await container.DeleteContainerAsync();
Mock.Get(factory.Object).Verify(f => f(), Times.Once);
}
finally
{
if (database!= null)
{
await database.DeleteAsync();
}
}
}
}

internal static class StringHelper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,13 @@ public async Task VerifyUserAgentWithFeatures(bool useMacOs)
this.SetEnvironmentInformation(useMacOs);

const string suffix = " UserApplicationName/1.0";
CosmosClientOptionsFeatures featuresFlags = CosmosClientOptionsFeatures.NoFeatures;
featuresFlags |= CosmosClientOptionsFeatures.AllowBulkExecution;
featuresFlags |= CosmosClientOptionsFeatures.HttpClientFactory;

string features = Convert.ToString((int)CosmosClientOptionsFeatures.AllowBulkExecution, 2).PadLeft(8, '0');
string features = Convert.ToString((int)featuresFlags, 2).PadLeft(8, '0');

using (CosmosClient client = TestCommon.CreateCosmosClient(builder => builder.WithApplicationName(suffix).WithBulkExecution(true)))
using (CosmosClient client = TestCommon.CreateCosmosClient(builder => builder.WithApplicationName(suffix).WithBulkExecution(true).WithHttpClientFactory(() => new HttpClient())))
{
Cosmos.UserAgentContainer userAgentContainer = client.ClientOptions.GetConnectionPolicy().UserAgentContainer;

Expand All @@ -130,7 +133,7 @@ public async Task VerifyUserAgentWithFeatures(bool useMacOs)
await db.DeleteAsync();
}

using (CosmosClient client = TestCommon.CreateCosmosClient(builder => builder.WithApplicationName(suffix).WithBulkExecution(false)))
using (CosmosClient client = TestCommon.CreateCosmosClient(builder => builder.WithApplicationName(suffix)))
{
Cosmos.UserAgentContainer userAgentContainer = client.ClientOptions.GetConnectionPolicy().UserAgentContainer;

Expand Down
Loading

0 comments on commit 5024c15

Please sign in to comment.