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

Diagnostics: Adds wrapper exception to include traces on ObjectDisposedException #2465

Merged
merged 12 commits into from
May 17, 2021
16 changes: 7 additions & 9 deletions Microsoft.Azure.Cosmos/src/CosmosClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public class CosmosClient : IDisposable
private bool isDisposed = false;

internal static int numberOfClientsCreated;
internal DateTime? DisposedDateTimeUtc { get; private set; } = null;

static CosmosClient()
{
Expand Down Expand Up @@ -506,7 +507,10 @@ internal CosmosClient(
/// </returns>
public virtual Task<AccountProperties> ReadAccountAsync()
{
return ((IDocumentClientInternal)this.DocumentClient).GetDatabaseAccountInternalAsync(this.Endpoint);
return this.ClientContext.OperationHelperAsync(
nameof(ReadAccountAsync),
null,
(trace) => ((IDocumentClientInternal)this.DocumentClient).GetDatabaseAccountInternalAsync(this.Endpoint));
}

/// <summary>
Expand Down Expand Up @@ -1262,6 +1266,8 @@ protected virtual void Dispose(bool disposing)
{
if (!this.isDisposed)
{
this.DisposedDateTimeUtc = DateTime.UtcNow;

if (disposing)
{
this.ClientContext.Dispose();
Expand All @@ -1270,13 +1276,5 @@ protected virtual void Dispose(bool disposing)
this.isDisposed = true;
}
}

private void ThrowIfDisposed()
{
if (this.isDisposed)
{
throw new ObjectDisposedException($"Accessing {nameof(CosmosClient)} after it is disposed is invalid.");
}
}
}
}
7 changes: 7 additions & 0 deletions Microsoft.Azure.Cosmos/src/Resource/ClientContextCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,13 @@ private async Task<TResult> RunWithDiagnosticsHelperAsync<TResult>(
{
throw new CosmosOperationCanceledException(oe, trace);
}
catch (ObjectDisposedException objectDisposed) when (!(objectDisposed is CosmosObjectDisposedException))
{
throw new CosmosObjectDisposedException(
objectDisposed,
this.client,
trace);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------

namespace Microsoft.Azure.Cosmos
{
using System;
using System.Collections;
using System.Globalization;
using Microsoft.Azure.Cosmos.Diagnostics;
using Microsoft.Azure.Cosmos.Tracing;

/// <summary>
/// The exception is a wrapper for ObjectDisposedExceptions. This wrapper
/// adds a way to access the CosmosDiagnostics and appends additional information
/// to the message for easier troubleshooting.
/// </summary>
internal class CosmosObjectDisposedException : ObjectDisposedException
{
private readonly ObjectDisposedException originalException;
private readonly CosmosClient cosmosClient;

/// <summary>
/// Create an instance of CosmosObjectDisposedException
/// </summary>
internal CosmosObjectDisposedException(
ObjectDisposedException originalException,
CosmosClient cosmosClient,
ITrace trace)
: base(originalException.ObjectName)
{
this.cosmosClient = cosmosClient ?? throw new ArgumentNullException(nameof(CosmosClient));
this.originalException = originalException ?? throw new ArgumentNullException(nameof(originalException));

string additionalInfo = $"CosmosClient Endpoint: {this.cosmosClient.Endpoint}; Created at: {this.cosmosClient.ClientConfigurationTraceDatum.ClientCreatedDateTimeUtc.ToString("o", CultureInfo.InvariantCulture)};" +
$" UserAgent: {this.cosmosClient.ClientConfigurationTraceDatum.UserAgentContainer.UserAgent};";
this.Message = this.cosmosClient.DisposedDateTimeUtc.HasValue
? $"Cannot access a disposed 'CosmosClient'. Follow best practices and use the CosmosClient as a singleton." +
$" CosmosClient was disposed at: {this.cosmosClient.DisposedDateTimeUtc.Value.ToString("o", CultureInfo.InvariantCulture)}; {additionalInfo}"
: $"{originalException.Message} The CosmosClient is still active and NOT disposed of. {additionalInfo}";

if (trace == null)
{
throw new ArgumentNullException(nameof(trace));
}

this.Diagnostics = new CosmosTraceDiagnostics(trace);
}

/// <inheritdoc/>
public override string Source
{
get => this.originalException.Source;
set => this.originalException.Source = value;
}

/// <inheritdoc/>
public override string Message { get; }

/// <inheritdoc/>
public override string StackTrace => this.originalException.StackTrace;

/// <inheritdoc/>
public override IDictionary Data => this.originalException.Data;

/// <summary>
/// Gets the diagnostics for the request
/// </summary>
public CosmosDiagnostics Diagnostics { get; }

/// <inheritdoc/>
public override string HelpLink
{
get => this.originalException.HelpLink;
set => this.originalException.HelpLink = value;
}

/// <inheritdoc/>
public override Exception GetBaseException()
{
return this.originalException.GetBaseException();
}

/// <inheritdoc/>
public override string ToString()
{
return $"{this.Message} {Environment.NewLine}CosmosDiagnostics: {this.Diagnostics} StackTrace: {this.StackTrace}";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------

namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests
{
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class CosmosUnexpectedExceptionTests : BaseCosmosClientHelper
{
private ContainerInternal Container = null;

[TestInitialize]
public async Task TestInitialize()
{
await base.TestInit();
string PartitionKey = "/pk";
ContainerResponse response = await this.database.CreateContainerAsync(
new ContainerProperties(id: Guid.NewGuid().ToString(), partitionKeyPath: PartitionKey),
cancellationToken: this.cancellationToken);
Assert.IsNotNull(response);
Assert.IsNotNull(response.Container);
Assert.IsNotNull(response.Resource);
this.Container = (ContainerInternal)response;
}

[TestCleanup]
public async Task Cleanup()
{
await base.TestCleanup();
}

[TestMethod]
public async Task CheckTracesIncludedWithAllExceptionsTestAsync()
{
RequestHandlerHelper requestHandlerHelper = new RequestHandlerHelper();
CosmosClient cosmosClient = TestCommon.CreateCosmosClient(
customizeClientBuilder: builder => builder.AddCustomHandlers(requestHandlerHelper));
Container containerWithFailure = cosmosClient.GetContainer(this.database.Id, this.Container.Id);

requestHandlerHelper.UpdateRequestMessage = (request) => throw new ObjectDisposedException("Mock ObjectDisposedException");
await this.CheckForTracesAsync<ObjectDisposedException>(containerWithFailure, isClientDisposed: false);

cosmosClient.Dispose();
requestHandlerHelper.UpdateRequestMessage = (request) => throw new ObjectDisposedException("Mock ObjectDisposedException");
await this.CheckForTracesAsync<ObjectDisposedException>(containerWithFailure, isClientDisposed: true);
}


private async Task CheckForTracesAsync<ExceptionType>(
Container container,
bool isClientDisposed) where ExceptionType : Exception
{
ToDoActivity toDoActivity = ToDoActivity.CreateRandomToDoActivity();

try
{
await container.CreateItemAsync<ToDoActivity>(
toDoActivity,
new Cosmos.PartitionKey(toDoActivity.pk));

Assert.Fail("Should have thrown");
}
catch (ExceptionType e)
{
if (isClientDisposed)
{
Assert.IsTrue(e.Message.Contains("Cannot access a disposed 'CosmosClient'. Please make sure to follow best practices and use the CosmosClient as a singleton."));
}
else
{
Assert.IsTrue(e.Message.Contains("The CosmosClient is still active and NOT disposed of. CosmosClient Endpoint:"));
}

Assert.IsFalse(e.Message.Contains("CosmosDiagnostics"));
Assert.IsTrue(e.ToString().Contains("Client Configuration"));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Microsoft.Azure.Cosmos.Tests
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
Expand All @@ -28,6 +29,11 @@ public async Task TestDispose()
TransactionalBatch batch = container.CreateTransactionalBatch(new PartitionKey("asdf"));
batch.ReadItem("Test");

FeedIterator<dynamic> feedIterator1 = container.GetItemQueryIterator<dynamic>();
FeedIterator<dynamic> feedIterator2 = container.GetItemQueryIterator<dynamic>(queryText: "select * from T");
FeedIterator<dynamic> feedIterator3 = database.GetContainerQueryIterator<dynamic>(queryText: "select * from T");

string userAgent = cosmosClient.ClientContext.UserAgent;
// Dispose should be idempotent
cosmosClient.Dispose();
cosmosClient.Dispose();
Expand All @@ -42,18 +48,32 @@ public async Task TestDispose()
() => container.Scripts.ReadTriggerAsync("asdf"),
() => container.Scripts.ReadUserDefinedFunctionAsync("asdf"),
() => batch.ExecuteAsync(),
() => container.GetItemQueryIterator<dynamic>(queryText: "select * from T").ReadNextAsync(),
() => container.GetItemQueryIterator<dynamic>().ReadNextAsync(),
() => feedIterator1.ReadNextAsync(),
() => feedIterator2.ReadNextAsync(),
() => feedIterator3.ReadNextAsync(),
};

foreach (Func<Task> asyncFunc in validateAsync)
{
try
{
await asyncFunc();
await asyncFunc();
Assert.Fail("Should throw ObjectDisposedException");
}
catch (ObjectDisposedException) { }
catch (CosmosObjectDisposedException e)
{
string expectedMessage = $"Cannot access a disposed 'CosmosClient'. Follow best practices and use the CosmosClient as a singleton." +
$" CosmosClient was disposed at: {cosmosClient.DisposedDateTimeUtc.Value.ToString("o", CultureInfo.InvariantCulture)}; CosmosClient Endpoint: https://localtestcosmos.documents.azure.com/; Created at: {cosmosClient.ClientConfigurationTraceDatum.ClientCreatedDateTimeUtc.ToString("o", CultureInfo.InvariantCulture)}; UserAgent: {userAgent};";
Assert.IsTrue(e.Message.Contains(expectedMessage));
string diagnostics = e.Diagnostics.ToString();
Assert.IsNotNull(diagnostics);
Assert.IsFalse(diagnostics.Contains("NoOp"));
Assert.IsTrue(diagnostics.Contains("Client Configuration"));
string exceptionString = e.ToString();
Assert.IsTrue(exceptionString.Contains(diagnostics));
Assert.IsTrue(exceptionString.Contains(e.Message));
Assert.IsTrue(exceptionString.Contains(e.StackTrace));
}
}
}

Expand Down