diff --git a/Microsoft.Azure.Cosmos/src/CosmosClient.cs b/Microsoft.Azure.Cosmos/src/CosmosClient.cs index b41e0aa2f8..b9c9c13fa0 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClient.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClient.cs @@ -100,8 +100,11 @@ public class CosmosClient : IDisposable private readonly string DatabaseRootUri = Paths.Databases_Root; private ConsistencyLevel? accountConsistencyLevel; private bool isDisposed = false; + private object disposedLock = new object(); internal static int numberOfClientsCreated; + internal static int NumberOfActiveClients; + internal DateTime? DisposedDateTimeUtc { get; private set; } = null; static CosmosClient() @@ -262,6 +265,7 @@ internal CosmosClient( clientOptions ??= new CosmosClientOptions(); this.ClientId = this.IncrementNumberOfClientsCreated(); + this.ClientContext = ClientContextCore.Create( this, clientOptions); @@ -1232,9 +1236,27 @@ private Task InitializeContainersAsync(IReadOnlyList<(string databaseId, string private int IncrementNumberOfClientsCreated() { + this.IncrementNumberOfActiveClients(); + return Interlocked.Increment(ref numberOfClientsCreated); } + private int IncrementNumberOfActiveClients() + { + return Interlocked.Increment(ref NumberOfActiveClients); + } + + private int DecrementNumberOfActiveClients() + { + // In case dispose is called multiple times. Check if at least 1 active client is there + if (NumberOfActiveClients > 0) + { + return Interlocked.Decrement(ref NumberOfActiveClients); + } + + return 0; + } + private async Task InitializeContainerAsync(string databaseId, string containerId, CancellationToken cancellationToken = default) { ContainerInternal container = (ContainerInternal)this.GetContainer(databaseId, containerId); @@ -1280,17 +1302,22 @@ public void Dispose() /// True if disposing protected virtual void Dispose(bool disposing) { - if (!this.isDisposed) + lock (this.disposedLock) { - this.DisposedDateTimeUtc = DateTime.UtcNow; - - if (disposing) + if (this.isDisposed == true) { - this.ClientContext.Dispose(); + return; } - this.isDisposed = true; } + + this.DisposedDateTimeUtc = DateTime.UtcNow; + + if (disposing) + { + this.ClientContext.Dispose(); + this.DecrementNumberOfActiveClients(); + } } } } diff --git a/Microsoft.Azure.Cosmos/src/Tracing/TraceData/ClientConfigurationTraceDatum.cs b/Microsoft.Azure.Cosmos/src/Tracing/TraceData/ClientConfigurationTraceDatum.cs index 353afccec3..7fa91b5fd5 100644 --- a/Microsoft.Azure.Cosmos/src/Tracing/TraceData/ClientConfigurationTraceDatum.cs +++ b/Microsoft.Azure.Cosmos/src/Tracing/TraceData/ClientConfigurationTraceDatum.cs @@ -28,6 +28,7 @@ public ClientConfigurationTraceDatum(CosmosClientContext cosmosClientContext, Da cosmosClientContext.ClientOptions.ApplicationPreferredRegions); this.cachedNumberOfClientCreated = CosmosClient.numberOfClientsCreated; + this.cachedNumberOfActiveClient = CosmosClient.NumberOfActiveClients; this.cachedUserAgentString = this.UserAgentContainer.UserAgent; this.cachedSerializedJson = this.GetSerializedDatum(); } @@ -46,9 +47,11 @@ public ReadOnlyMemory SerializedJson { get { - if ((this.cachedUserAgentString != this.UserAgentContainer.UserAgent) || - (this.cachedNumberOfClientCreated != CosmosClient.numberOfClientsCreated)) + if (this.cachedUserAgentString != this.UserAgentContainer.UserAgent || + this.cachedNumberOfClientCreated != CosmosClient.numberOfClientsCreated || + this.cachedNumberOfActiveClient != CosmosClient.NumberOfActiveClients) { + this.cachedNumberOfActiveClient = CosmosClient.NumberOfActiveClients; this.cachedNumberOfClientCreated = CosmosClient.numberOfClientsCreated; this.cachedUserAgentString = this.UserAgentContainer.UserAgent; this.cachedSerializedJson = this.GetSerializedDatum(); @@ -62,6 +65,8 @@ public ReadOnlyMemory SerializedJson private ReadOnlyMemory cachedSerializedJson; private int cachedNumberOfClientCreated; + private int cachedNumberOfActiveClient; + private string cachedUserAgentString; internal override void Accept(ITraceDatumVisitor traceDatumVisitor) @@ -79,6 +84,8 @@ private ReadOnlyMemory GetSerializedDatum() jsonTextWriter.WriteFieldName("NumberOfClientsCreated"); jsonTextWriter.WriteNumber64Value(this.cachedNumberOfClientCreated); + jsonTextWriter.WriteFieldName("NumberOfActiveClients"); + jsonTextWriter.WriteNumber64Value(this.cachedNumberOfActiveClient); jsonTextWriter.WriteFieldName("User Agent"); jsonTextWriter.WriteStringValue(this.cachedUserAgentString); diff --git a/Microsoft.Azure.Cosmos/src/Tracing/TraceWriter.TraceTextWriter.cs b/Microsoft.Azure.Cosmos/src/Tracing/TraceWriter.TraceTextWriter.cs index caf0587d06..8fc6b214c7 100644 --- a/Microsoft.Azure.Cosmos/src/Tracing/TraceWriter.TraceTextWriter.cs +++ b/Microsoft.Azure.Cosmos/src/Tracing/TraceWriter.TraceTextWriter.cs @@ -479,6 +479,7 @@ public void Visit(ClientConfigurationTraceDatum clientConfigurationTraceDatum) stringBuilder.AppendLine("Client Configuration"); stringBuilder.AppendLine($"Client Created Time: {clientConfigurationTraceDatum.ClientCreatedDateTimeUtc.ToString("o", CultureInfo.InvariantCulture)}"); stringBuilder.AppendLine($"Number Of Clients Created: {CosmosClient.numberOfClientsCreated}"); + stringBuilder.AppendLine($"Number Of Active Clients: {CosmosClient.NumberOfActiveClients}"); stringBuilder.AppendLine($"User Agent: {clientConfigurationTraceDatum.UserAgentContainer.UserAgent}"); stringBuilder.AppendLine("Connection Config:"); stringBuilder.AppendLine($"{space}'gw': {clientConfigurationTraceDatum.GatewayConnectionConfig}"); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ActiveClientDiagnosticTest.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ActiveClientDiagnosticTest.cs new file mode 100644 index 0000000000..348107434e --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ActiveClientDiagnosticTest.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos.Tests +{ + using System; + using System.Threading.Tasks; + using System.Xml.Serialization; + using VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class ActiveClientDiagnosticTest + { + private const string ConnectionString = "AccountEndpoint=https://localtestcosmos.documents.azure.com:443/;AccountKey=425Mcv8CXQqzRNCgFNjIhT424GK99CKJvASowTnq15Vt8LeahXTcN5wt3342vQ==;"; + + [TestInitialize] + public void Initialize() + { + CosmosClient.NumberOfActiveClients = 0; + } + + [TestMethod] + public void SingleClientTest() + { + CosmosClient cosmosClient = new CosmosClient(ConnectionString); + Assert.AreEqual(1, CosmosClient.NumberOfActiveClients); + cosmosClient.Dispose(); + } + + [TestMethod] + public void MultiClientTest() + { + CosmosClient cosmosClient1 = new CosmosClient(ConnectionString); // Initializing 1st time + CosmosClient cosmosClient2 = new CosmosClient(ConnectionString); // Initializing 2nd time + Assert.AreEqual(2, CosmosClient.NumberOfActiveClients); + cosmosClient1.Dispose(); + cosmosClient2.Dispose(); + } + + [TestMethod] + public void MultiClientWithDisposeTest() + { + CosmosClient cosmosClient1 = new CosmosClient(ConnectionString); // Initializing 1st time + CosmosClient cosmosClient2 = new CosmosClient(ConnectionString); // Initializing 2nd time + CosmosClient cosmosClient3 = new CosmosClient(ConnectionString); // Initializing 3rd time + cosmosClient2.Dispose(); // Destroying 1 instance + Assert.AreEqual(2, CosmosClient.NumberOfActiveClients); + cosmosClient1.Dispose(); + cosmosClient3.Dispose(); + } + + [TestMethod] + public void MultiThreadClientDisposeTest() + { + CosmosClient cosmosClient1 = new CosmosClient(ConnectionString); // Initializing 1st time + CosmosClient cosmosClient2 = new CosmosClient(ConnectionString); + + Parallel.Invoke(cosmosClient2.Dispose, + cosmosClient2.Dispose, + cosmosClient2.Dispose, + cosmosClient2.Dispose, + cosmosClient2.Dispose); + + // Initializing 2nd time + Assert.AreEqual(1, CosmosClient.NumberOfActiveClients); + cosmosClient1.Dispose(); + cosmosClient2.Dispose(); + } + } +} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DefaultTracingTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DefaultTracingTests.cs index c17b9344de..011bdfe415 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DefaultTracingTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DefaultTracingTests.cs @@ -19,8 +19,9 @@ public class DefaultTracingTests [TestMethod] public void DefaultTracingEnableTest() { - // Access cosmos client to cause the static consturctor to get called + // Access cosmos client to cause the static constructor to get called Assert.IsTrue(CosmosClient.numberOfClientsCreated >= 0); + Assert.IsTrue(CosmosClient.NumberOfActiveClients >= 0); if (!Debugger.IsAttached) {