GraphServiceClient creates ActivitySource objects that are never disposed
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 52/100
- Issue 类型
- 缺陷
- 描述清晰度
- 基本清楚
- 活跃度
- 冷清
- 技术栈
- azure, csharp
- 领域
- api, backend, performance
调研方向
从释放 GraphServiceClient 开始,沿着 BaseGraphRequestAdapter、AzureIdentityAuthenticationProvider 和 AzureIdentityAccessTokenProvider 追踪整个链路。运行提供的控制台复现程序,重复创建和释放客户端,然后验证释放后 Microsoft.Kiota.Authentication.Azure ActivitySource 的计数恢复为零。完成标准是:无需 workaround 即可释放客户端拥有的身份验证资源。
由索引模型根据 Issue 内容生成。
描述
Describe the bug
In our Web application, we use the .NET Graph SDK and create GraphServiceClient objects in many requests. These clients are created by scoped services, and disposed when the associated scope is disposed.
We have seen a memory and CPU increase in our web application over days. A memory dump analysis showed that a huge number of ActivitySource objects with name Microsoft.Kiota.Authentication.Azure are created and never released. Here is the result of the command dotnet-dump analyze <file>.dmp, then dumpheap -stat -live:
Most of these 644,957 ActivitySource objects are named Microsoft.Kiota.Authentication.Azure:
Some more investigation showed that one new ActivitySource is created each time a GraphServiceClient is created.
Expected behavior
A singleton ActivitySource object is created, or the ActivitySource objects are disposed when GraphServiceClient is disposed.
How to reproduce
Create a repro project with the commands
dotnet new console
dotnet add package Microsoft.Graph Azure.Identity
Set the Program.cs source code as:
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.Graph.Authentication;
using Microsoft.Kiota.Abstractions.Authentication;
using System.Diagnostics;
using System.Reflection;
var scopes = new[] { "User.Read" };
var options = new InteractiveBrowserCredentialOptions
{
TenantId = "YOUR_TENANT_ID",
ClientId = "YOUR_CLIENT_ID",
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
RedirectUri = new Uri("http://localhost"),
};
var interactiveCredential = new InteractiveBrowserCredential(options);
await GetCurrentUserUserNameAsync(1);
await GetCurrentUserUserNameAsync(2);
async Task GetCurrentUserUserNameAsync(int iteration)
{
PrintMicrosoftKiotaAbstractionsActivitySourceCount($"Iteration {iteration}: Before creating GraphServiceClient.");
using (var graphServiceClient = GetGraphServiceClient(scopes, interactiveCredential, useWorkaround: false, out IDisposable? additionalDisposable))
{
var user = await graphServiceClient.Me.GetAsync();
Console.WriteLine($"Hello, {user?.DisplayName}!");
PrintMicrosoftKiotaAbstractionsActivitySourceCount($"Iteration {iteration}: After request, before disposing GraphServiceClient.");
additionalDisposable?.Dispose();
}
PrintMicrosoftKiotaAbstractionsActivitySourceCount($"Iteration {iteration}: After disposing GraphServiceClient.");
}
void PrintMicrosoftKiotaAbstractionsActivitySourceCount(string message)
{
FieldInfo? activeSourcesField = typeof(ActivitySource).GetField("s_activeSources", BindingFlags.NonPublic | BindingFlags.Static);
object? synchronizedListInstance = activeSourcesField?.GetValue(null) ?? throw new InvalidOperationException("s_activeSources field not found.");
Type synchronizedListType = synchronizedListInstance.GetType();
FieldInfo? arrayField = synchronizedListType.GetField("_volatileArray", BindingFlags.NonPublic | BindingFlags.Instance);
var sources = (ActivitySource[])(arrayField?.GetValue(synchronizedListInstance) ?? throw new InvalidOperationException("_volatileArray field not found."));
var count = sources.Count((ActivitySource s) => s.Name == "Microsoft.Kiota.Authentication.Azure");
Console.WriteLine($"{message} There are {count} activity sources named 'Microsoft.Kiota.Authentication.Azure'.");
}
static GraphServiceClient GetGraphServiceClient(string[] scopes, InteractiveBrowserCredential interactiveCredential, bool useWorkaround, out IDisposable? additionalDisposable)
{
additionalDisposable = null;
if (useWorkaround)
{
var appOnlyAccessTokenProvider = new AzureIdentityAccessTokenProvider(interactiveCredential, null, null, true);
var tokenAuthenticationProvider = new BaseBearerTokenAuthenticationProvider(appOnlyAccessTokenProvider);
additionalDisposable = appOnlyAccessTokenProvider;
return new GraphServiceClient(tokenAuthenticationProvider);
}
else
{
return new GraphServiceClient(interactiveCredential, scopes);
}
}
In an Entra ID tenant, create an Entra ID application with:
- Supported account types: Single tenant only
- Redirect URI: Public client/native, URI: http://localhost
Copy the Client ID and Tenant IDs into the Program.cs source code and run the application with:
dotnet run
The application Creates twice a GraphServiceClient instance and displays the name of the current user. It also uses reflection to show the number of ActivitySource objects named Microsoft.Kiota.Authentication.Azure being created. The output looks like:
Iteration 1: Before creating GraphServiceClient. There are 0 activity sources named 'Microsoft.Kiota.Authentication.Azure'.
Hello, <your name>!
Iteration 1: After request, before disposing GraphServiceClient. There are 1 activity sources named 'Microsoft.Kiota.Authentication.Azure'.
Iteration 1: After disposing GraphServiceClient. There are 1 activity sources named 'Microsoft.Kiota.Authentication.Azure'.
Iteration 2: Before creating GraphServiceClient. There are 1 activity sources named 'Microsoft.Kiota.Authentication.Azure'.
Hello, <your name>!
Iteration 2: After request, before disposing GraphServiceClient. There are 2 activity sources named 'Microsoft.Kiota.Authentication.Azure'.
Iteration 2: After disposing GraphServiceClient. There are 2 activity sources named 'Microsoft.Kiota.Authentication.Azure'.
As can be seen, two ActivitySource objects have been created and will never be released, and one more will be added each time a new GraphServiceClient object is created, even if it's properly disposed.
SDK Version
6.2.0
Latest version known to work for scenario above?
No response
Known Workarounds
The workaround is to create explicitly the Microsoft.Graph.Authentication.AzureIdentityAccessTokenProvider and dispose it. In that case the ActivitySource is disposed properly. This can be seen in the sample code by setting useWorkaround: true instead of useWorkaround: false, The output is then:
Iteration 1: Before creating GraphServiceClient. There are 0 activity sources named 'Microsoft.Kiota.Authentication.Azure'.
Hello, <your name>!
Iteration 1: After request, before disposing GraphServiceClient. There are 1 activity sources named 'Microsoft.Kiota.Authentication.Azure'.
Iteration 1: After disposing GraphServiceClient. There are 0 activity sources named 'Microsoft.Kiota.Authentication.Azure'.
Iteration 2: Before creating GraphServiceClient. There are 0 activity sources named 'Microsoft.Kiota.Authentication.Azure'.
Hello, <your name>!
Iteration 2: After request, before disposing GraphServiceClient. There are 1 activity sources named 'Microsoft.Kiota.Authentication.Azure'.
Iteration 2: After disposing GraphServiceClient. There are 0 activity sources named 'Microsoft.Kiota.Authentication.Azure'.
We see that the ActivitySource are created with the same lifetime as the GraphServiceClient, and are properly disposed.
Debug output
See above for details
Configuration
- OS: Windows 11
- Platform: .NET 10
- Architecture: x64
The problem is reproducible in our Azure App Service as well as on local developer laptops.
Other information
The GraphServiceClient constructor public GraphServiceClient(IRequestAdapter requestAdapter, string baseUrl = null) constructs the following:
- a
GraphServiceClientinstance with:- property
RequestAdapterof typeMicrosoft.Kiota.Abstractions.IRequestAdapter. Runtime type:Microsoft.Graph.BaseGraphRequestAdapterobject - implements
IDisposableand properly disposes RequestAdapter
- property
- The
Microsoft.Graph.BaseGraphRequestAdapterinstance:- has a field
authProviderof typeMicrosoft.Kiota.Abstractions.Authentication.IAuthenticationProvider. Runtime type:Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider - implements
IDisposablebutDispose()doesn't disposeauthProvider.
- has a field
- The
Microsoft.Graph.Authentication.AzureIdentityAuthenticationProviderinstance- has a property
AccessTokenProviderof typeMicrosoft.Kiota.Abstractions.Authentication.IAccessTokenProvider. Runtime type:Microsoft.Graph.Authentication.AzureIdentityAccessTokenProvider - doesn't implement
IDisposable, so cannot disposeAccessTokenProvider
- has a property
- The
Microsoft.Graph.Authentication.AzureIdentityAccessTokenProviderinstance- has a field
_activitySourceof typeSystem.Diagnostics.ActivitySource, namedMicrosoft.Kiota.Authentication.Azure - implements
IDisposableand properly disposes_activitySource
- has a field
Fix
Microsoft.Graph.BaseGraphRequestAdaptermust disposeauthProviderMicrosoft.Graph.Authentication.AzureIdentityAccessTokenProvidermust implementIDisposableand disposeauthProvider
Alternatively, the ActivitySource named Microsoft.Kiota.Authentication.Azure could be created as a static readonly field and be reused by all Microsoft.Graph.Authentication.AzureIdentityAccessTokenProvider instances.
- 主要语言
- C#
- 星标
- 791
- 派生
- 264
- 平均合并
- 5 小时 25 分钟
- 30 天内合并 PR
- 5
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
microsoftgraph/msgraph-sdk-dotnet 的其他 Issue
-
status:waiting-for-triage type:bug
难度 2/5 1-3 小时 新手友好度 68/100
microsoftgraph/msgraph-sdk-dotnet#3170 ·
维护者通常 1 天内回复
-
status:waiting-for-triage type:bug
难度 2/5 1-3 小时 新手友好度 72/100
microsoftgraph/msgraph-sdk-dotnet#3165 · 2 条评论 · 1 个 reaction ·
维护者通常 1 天内回复
-
难度 1/5 1 小时以内 新手友好度 85/100
microsoftgraph/msgraph-sdk-dotnet#3159 ·
维护者通常 1 天内回复
-
Bug: metadata
难度 2/5 1-3 小时 新手友好度 68/100
microsoftgraph/msgraph-sdk-dotnet#3004 · 1 条评论 ·
维护者通常 1 天内回复
-
Status: Needs investigation type:bug
难度 2/5 1-3 小时 新手友好度 64/100
microsoftgraph/msgraph-sdk-dotnet#2858 · 2 条评论 ·
维护者通常 1 天内回复
查看 microsoftgraph/msgraph-sdk-dotnet 的全部 Issue
相似的 Issue
-
python triage
难度 2/5 1-3 小时 新手友好度 72/100
microsoft/semantic-kernel#14615 ·
维护者通常 3 天内回复
-
难度 2/5 1-3 小时 新手友好度 75/100
vzeol/DeployCraft#125 ·
维护者通常 1 天内回复
-
bug
难度 2/5 1-3 小时 新手友好度 76/100
fboucher/2d6-dungeon-app#245 · 1 条评论 ·
维护者通常 1 天内回复
-
area: settings good first issue priority: p2 type: bug
难度 2/5 1-3 小时 新手友好度 82/100
AmineChr54/umbra#3 ·
维护者通常 1 天内回复
-
needs-triage
难度 2/5 1-3 小时 新手友好度 68/100
维护者通常 1 天内回复