Skip to content
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

Transport: Add HttpClientFactory support #1441

Merged
merged 11 commits into from
May 1, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
kirankumarkolli marked this conversation as resolved.
Show resolved Hide resolved
/// </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
36 changes: 35 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.
ealsur marked this conversation as resolved.
Show resolved Hide resolved
/// </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
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,44 @@ 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
{
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 @@ -21,6 +21,7 @@ public class CosmosClientOptionsUnitTests
{
public const string AccountEndpoint = "https://localhost:8081/";
public const string ConnectionString = "AccountEndpoint=https://localtestcosmos.documents.azure.com:443/;AccountKey=425Mcv8CXQqzRNCgFNjIhT424GK99CKJvASowTnq15Vt8LeahXTcN5wt3342vQ==;";
public Func<HttpClient> HttpClientFactoryDelegate = () => new HttpClient();

[TestMethod]
public void VerifyCosmosConfigurationPropertiesGetUpdated()
Expand Down Expand Up @@ -74,6 +75,7 @@ public void VerifyCosmosConfigurationPropertiesGetUpdated()
Assert.IsNull(clientOptions.WebProxy);
Assert.IsFalse(clientOptions.LimitToEndpoint);
Assert.IsFalse(clientOptions.EnableTcpConnectionEndpointRediscovery);
Assert.IsNull(clientOptions.HttpClientFactory);

//Verify GetConnectionPolicy returns the correct values for default
ConnectionPolicy policy = clientOptions.GetConnectionPolicy();
Expand All @@ -87,6 +89,7 @@ public void VerifyCosmosConfigurationPropertiesGetUpdated()
Assert.IsNull(policy.MaxTcpConnectionsPerEndpoint);
Assert.IsTrue(policy.EnableEndpointDiscovery);
Assert.IsFalse(policy.EnableTcpConnectionEndpointRediscovery);
Assert.IsNull(policy.HttpClientFactory);

cosmosClientBuilder.WithApplicationRegion(region)
.WithConnectionModeGateway(maxConnections, webProxy)
Expand Down Expand Up @@ -353,6 +356,41 @@ public void VerifyApplicationRegionSettingsWithPreferredRegions()
cosmosClientOptions.GetConnectionPolicy();
}

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void VerifyWebProxyHttpClientFactorySet()
{
CosmosClientOptions cosmosClientOptions = new CosmosClientOptions();
cosmosClientOptions.WebProxy = Mock.Of<WebProxy>();
cosmosClientOptions.HttpClientFactory = () => new HttpClient();
}

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void VerifyHttpClientFactoryWebProxySet()
{
CosmosClientOptions cosmosClientOptions = new CosmosClientOptions();
cosmosClientOptions.HttpClientFactory = () => new HttpClient();
cosmosClientOptions.WebProxy = Mock.Of<WebProxy>();
}

[TestMethod]
public void HttpClientFactoryBuildsConnectionPolicy()
{
string endpoint = AccountEndpoint;
string key = Guid.NewGuid().ToString();
CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder(
accountEndpoint: endpoint,
authKeyOrResourceToken: key)
.WithHttpClientFactory(this.HttpClientFactoryDelegate);
CosmosClient cosmosClient = cosmosClientBuilder.Build(new MockDocumentClient());
CosmosClientOptions clientOptions = cosmosClient.ClientOptions;

Assert.AreEqual(clientOptions.HttpClientFactory, this.HttpClientFactoryDelegate);
ConnectionPolicy policy = clientOptions.GetConnectionPolicy();
Assert.AreEqual(policy.HttpClientFactory, this.HttpClientFactoryDelegate);
}

[TestMethod]
public void WithLimitToEndpointAffectsEndpointDiscovery()
{
Expand Down
Loading