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>
public class CosmosObjectDisposedException : ObjectDisposedException
j82w marked this conversation as resolved.
Show resolved Hide resolved
j82w marked this conversation as resolved.
Show resolved Hide resolved
{
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 disposedMessage = this.cosmosClient.DisposedDateTimeUtc.HasValue
? $" Disposed at: {this.cosmosClient.DisposedDateTimeUtc.Value.ToString("o", CultureInfo.InvariantCulture)}"
: " NOT disposed";

this.Message = $"{originalException.Message} CosmosClient Endpoint: {this.cosmosClient.Endpoint}; Created at: {this.cosmosClient.ClientConfigurationTraceDatum.ClientCreatedDateTimeUtc.ToString("o", CultureInfo.InvariantCulture)}; " +
$"{disposedMessage}; UserAgent: {this.cosmosClient.ClientConfigurationTraceDatum.UserAgentContainer.UserAgent};";

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
Expand Up @@ -1436,7 +1436,7 @@
│ └── Change Feed Transport(00000000-0000-0000-0000-000000000000) Transport-Component MemberName@FilePath:42 12:00:00:000 0.00 milliseconds
└── MoveNextAsync(00000000-0000-0000-0000-000000000000) Pagination-Component MemberName@FilePath:42 12:00:00:000 0.00 milliseconds
└── [9F-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF,BF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF) move next(00000000-0000-0000-0000-000000000000) Pagination-Component MemberName@FilePath:42 12:00:00:000 0.00 milliseconds
└── Change Feed Transport(00000000-0000-0000-0000-000000000000) Transport-Component MemberName@FilePath:42 12:00:00:000 0.00 milliseconds
└── Change Feed Transport(00000000-0000-0000-0000-000000000000) Transport-Component MemberName@FilePath:42 12:00:00:000 0.00 milliseconds
j82w marked this conversation as resolved.
Show resolved Hide resolved
]]></Text>
<Json><![CDATA[{
"name": "Trace For Baseline Testing",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,96 @@
},
"NestedTypes": {}
},
"Microsoft.Azure.Cosmos.CosmosObjectDisposedException;System.ObjectDisposedException;IsAbstract:False;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": {
"Subclasses": {},
"Members": {
"Microsoft.Azure.Cosmos.CosmosDiagnostics Diagnostics": {
"Type": "Property",
"Attributes": [],
"MethodInfo": "Microsoft.Azure.Cosmos.CosmosDiagnostics Diagnostics;CanRead:True;CanWrite:False;Microsoft.Azure.Cosmos.CosmosDiagnostics get_Diagnostics();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"Microsoft.Azure.Cosmos.CosmosDiagnostics get_Diagnostics()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": {
"Type": "Method",
"Attributes": [
"CompilerGeneratedAttribute"
],
"MethodInfo": "Microsoft.Azure.Cosmos.CosmosDiagnostics get_Diagnostics();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"System.Collections.IDictionary Data": {
"Type": "Property",
"Attributes": [],
"MethodInfo": "System.Collections.IDictionary Data;CanRead:True;CanWrite:False;System.Collections.IDictionary get_Data();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"System.Collections.IDictionary get_Data()": {
"Type": "Method",
"Attributes": [],
"MethodInfo": "System.Collections.IDictionary get_Data();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"System.Exception GetBaseException()": {
"Type": "Method",
"Attributes": [],
"MethodInfo": "System.Exception GetBaseException();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"System.String get_HelpLink()": {
"Type": "Method",
"Attributes": [],
"MethodInfo": "System.String get_HelpLink();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"System.String get_Message()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": {
"Type": "Method",
"Attributes": [
"CompilerGeneratedAttribute"
],
"MethodInfo": "System.String get_Message();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"System.String get_Source()": {
"Type": "Method",
"Attributes": [],
"MethodInfo": "System.String get_Source();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"System.String get_StackTrace()": {
"Type": "Method",
"Attributes": [],
"MethodInfo": "System.String get_StackTrace();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"System.String HelpLink": {
"Type": "Property",
"Attributes": [],
"MethodInfo": "System.String HelpLink;CanRead:True;CanWrite:True;System.String get_HelpLink();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;Void set_HelpLink(System.String);IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"System.String Message": {
"Type": "Property",
"Attributes": [],
"MethodInfo": "System.String Message;CanRead:True;CanWrite:False;System.String get_Message();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"System.String Source": {
"Type": "Property",
"Attributes": [],
"MethodInfo": "System.String Source;CanRead:True;CanWrite:True;System.String get_Source();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;Void set_Source(System.String);IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"System.String StackTrace": {
"Type": "Property",
"Attributes": [],
"MethodInfo": "System.String StackTrace;CanRead:True;CanWrite:False;System.String get_StackTrace();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"System.String ToString()": {
"Type": "Method",
"Attributes": [],
"MethodInfo": "System.String ToString();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"Void set_HelpLink(System.String)": {
"Type": "Method",
"Attributes": [],
"MethodInfo": "Void set_HelpLink(System.String);IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"Void set_Source(System.String)": {
"Type": "Method",
"Attributes": [],
"MethodInfo": "Void set_Source(System.String);IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
}
},
"NestedTypes": {}
},
"Microsoft.Azure.Cosmos.CosmosOperationCanceledException;System.OperationCanceledException;IsAbstract:False;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": {
"Subclasses": {},
"Members": {
Expand Down
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,30 @@ 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)
{
Assert.IsTrue(e.Message.Contains($"CosmosClient Endpoint: https://localtestcosmos.documents.azure.com/; Created at: {cosmosClient.ClientConfigurationTraceDatum.ClientCreatedDateTimeUtc.ToString("o", CultureInfo.InvariantCulture)}; Disposed at: {cosmosClient.DisposedDateTimeUtc.Value.ToString("o", CultureInfo.InvariantCulture)}; UserAgent: {userAgent};"));
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