Detect circular dependencies in factory-based service registrations at runtime#124331
Detect circular dependencies in factory-based service registrations at runtime#124331
Conversation
|
@copilot detecting circular dependencies at build time (in CallSiteFactory) is sensible and would turn a silent deadlock into a clear exception. However, this is a detection fix, not a resolution fix, it would throw instead of deadlock, but the user's service graph would still be broken. Is that right? apps can work around this by breaking the cycle by injecting public ConfigureAppInsightsOptions(IServiceProvider sp)
{
_sp = sp;
}
public void Configure(ApplicationInsightsServiceOptions options)
{
var client = _sp.GetRequiredService<SecretClient>();
options.ConnectionString = client.GetSecret("appinsights").Value;
}If decided to do something here, would it make sense to do it with |
You're correct - I haven't implemented anything yet, only validated the repro per the original instructions. Your suggestion to add circular dependency detection in
The workaround pattern you mentioned (injecting Should I proceed with implementing circular dependency detection in the validation path ( |
Yes, go ahead and do it. |
Started implementation. Added test case to reproduce factory-based circular dependency (issue #88390). The test demonstrates that factory functions can hide circular dependencies that aren't detected during call site construction, leading to deadlocks at resolution time. |
- Added FactoryCircularDependencyA and FactoryCircularDependencyB fake classes - Added test case FactoryCircularDependency_DetectedWithValidateOnBuild - Test demonstrates circular dependency through factory functions (issue #88390) - Currently ValidateOnBuild doesn't detect factory-based circular dependencies Co-authored-by: tarekgh <10833894+tarekgh@users.noreply.github.com>
Modify ValidateService() to actually resolve singleton services during ValidateOnBuild. This detects circular dependencies that occur through factory functions (issue #88390). When a factory function calls IServiceProvider.GetService(), circular dependencies weren't previously detected during call site construction. Now, by resolving singletons during validation, these circular dependencies are exposed before they cause deadlocks at runtime. Co-authored-by: tarekgh <10833894+tarekgh@users.noreply.github.com>
- Remove unnecessary catch block in ValidateService() - Add documentation to FactoryCircularDependencyB test helper - Improve test assertions to specifically check for circular dependency errors - Test now verifies proper error message in both validateOnBuild modes Co-authored-by: tarekgh <10833894+tarekgh@users.noreply.github.com>
|
@copilot looking more, seems the current approach has fundamental issues as resolving singletons during ValidateOnBuild is wrong because: The PR adds CallSiteRuntimeResolver.Instance.Resolve(callSite, Root) inside ValidateService() for all singleton services. This eagerly instantiates every singleton at build time, which introduces several problems:
We'll go back to have the fix in the runtime resolution path instead Maybe we can implement the re-entrance detection in [ThreadStatic]
private static HashSet<ServiceCallSite>? t_resolving;
protected override object? VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
{
if (callSite.Value is object value)
return value;
var lockType = RuntimeResolverLock.Root;
ServiceProviderEngineScope serviceProviderEngine = context.Scope.RootProvider.Root;
lock (callSite)
{
if (callSite.Value is object callSiteValue)
return callSiteValue;
t_resolving ??= new HashSet<ServiceCallSite>(ReferenceEqualityComparer.Instance);
if (!t_resolving.Add(callSite))
{
throw new InvalidOperationException(
SR.Format(SR.CircularDependencyException, callSite.ServiceType));
}
try
{
object? resolved = VisitCallSiteMain(callSite, new RuntimeResolverContext
{
Scope = serviceProviderEngine,
AcquiredLocks = context.AcquiredLocks | lockType
});
serviceProviderEngine.CaptureDisposable(resolved);
callSite.Value = resolved;
return resolved;
}
finally
{
t_resolving.Remove(callSite);
}
}
}Could you check if this approach will work and will not have any side effects? |
…tracking Per @tarekgh's feedback, reverted ValidateOnBuild approach which had fundamental issues: - Eagerly instantiating singletons caused side effects - Changed singleton lifetime semantics - Caused ordering issues New approach implements re-entrance detection at resolution time: - Added [ThreadStatic] HashSet to track call sites being resolved on current thread - Detects circular dependencies in VisitRootCache before deadlock occurs - Uses custom ReferenceEqualityComparer for ServiceCallSite instances - Throws InvalidOperationException with clear error message when cycle detected This addresses issue #88390 without changing validation behavior or singleton semantics. Co-authored-by: tarekgh <10833894+tarekgh@users.noreply.github.com>
You're absolutely right about the fundamental issues with the ValidateOnBuild approach. I've reverted that and implemented the re-entrance detection in Changes in commit aa55803:
This approach:
|
...raries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteRuntimeResolver.cs
Outdated
Show resolved
Hide resolved
Per @svick's feedback, removed custom ReferenceEqualityComparer and now use the shared implementation from CoreLib: - Added reference to $(CoreLibSharedDir)System\Collections\Generic\ReferenceEqualityComparer.cs - Changed HashSet<ServiceCallSite> to HashSet<object> to work with shared comparer - Removed custom ReferenceEqualityComparer nested class - Uses System.Collections.Generic.ReferenceEqualityComparer.Instance This reduces code duplication and uses the well-tested shared implementation. Co-authored-by: rosebyte <14963300+rosebyte@users.noreply.github.com>
Head branch was pushed to by a user without write access
Done in commit 5a65eb0. Replaced custom ReferenceEqualityComparer with the shared implementation from CoreLib. Added compile reference to |
|
@copilot |
Per @svick's feedback, reverted to HashSet<ServiceCallSite> instead of HashSet<object>. IEqualityComparer<T> is contravariant, so IEqualityComparer<object> can be used as IEqualityComparer<ServiceCallSite>, providing better type safety while still using the shared ReferenceEqualityComparer.Instance from CoreLib. Co-authored-by: svick <287848+svick@users.noreply.github.com>
Head branch was pushed to by a user without write access
Excellent point! Fixed in commit f6e3106. Reverted to |
Description
Fixes issue #88390 - Deadlock when configuring ApplicationInsightsServiceOptions due to circular dependencies through factory functions.
Problem
When services are registered using factory functions that call
IServiceProvider.GetService(), circular dependencies can cause deadlocks at runtime when the same lock is acquired recursively inCallSiteRuntimeResolver.VisitRootCache().Solution
Implemented re-entrance detection in
VisitRootCacheusing a[ThreadStatic]HashSet to track call sites currently being resolved on each thread. When a circular dependency is detected (attempting to resolve a call site already in the resolution chain), anInvalidOperationExceptionis thrown with a clear error message instead of deadlocking.Changes Made
CallSiteRuntimeResolver.cs: Added re-entrance detection
[ThreadStatic] HashSet<ServiceCallSite>to track call sites being resolved on current threadReferenceEqualityComparer.Instancefrom CoreLib (leveraging IEqualityComparer contravariance for type safety)VisitRootCache()to:Microsoft.Extensions.DependencyInjection.csproj: Reference shared ReferenceEqualityComparer
$(CoreLibSharedDir)System\Collections\Generic\ReferenceEqualityComparer.csTest Coverage: Added comprehensive tests for factory-based circular dependencies
FactoryCircularDependency_DetectedAtResolutionTime- verifies detection at service resolutionFactoryCircularDependency_NotDetectedByValidateOnBuild- documents that ValidateOnBuild cannot detect factory cyclesTest Helper Classes: Added fake classes for circular dependency testing
FactoryCircularDependencyA- Service with constructor dependency on BFactoryCircularDependencyB- Service registered via factory that creates circular dependencyDesign Decisions
[ThreadStatic]to avoid cross-thread interferenceReferenceEqualityComparerfrom CoreLib via contravariance for type-safe trackingSR.CircularDependencyExceptionresource stringBenefits
HashSet<ServiceCallSite>with contravariantIEqualityComparer<object>Testing
Original prompt
This section details on the original issue you should resolve
<issue_title>Deadlock when configuring ApplicationInsightsServiceOptions</issue_title>
<issue_description>### Description
When adding ApplicationInsights to an ASP.NET core web app's
IServiceCollectionviaAddApplicationInsightsTelemetry()and adding a call toConfigureOptions<ConfigureApplicationInsightsServiceOptions>()to configureApplicationInsightsServiceOptions, we are experiencing a deadlock.Sample code
Program.cs:
ConfigureApplicationInsightsServiceOptions.cs:
Reproduction Steps
ConfigureOptions<ConfigureApplicationInsightsServiceOptions>()on theIServiceCollectionas shown in the descriptionAddApplicationInsightsTelemetry()on theIServiceCollectionas shown in the descriptionOr see here: https://github.com/jonathan-vogel-siemens/IssueRepro88390
Expected behavior
ConfigureApplicationInsightsServiceOptions.Configure should be invoked when the
WebApplicationBuilderis buildingActual behavior
Deadlock. See running tasks here:
Somehow it is blocking at
runtime/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteRuntimeResolver.cs
Line 84 in 7575716
Regression?
Seems like it is a regression. A similar method (yielding the same deadlock) is documented here, from 2021: https://www.uveta.io/categories/blog/unclutter-startup-cs/#Application-Insights
Known Workarounds
No response
Configuration
Other information
No response</issue_description>
<agent_instructions>go and validate the provided repro in the link, don't do anything else, don't implement anything now</agent_instructions>
Comments on the Issue (you are @copilot in this section)
@ Tagging subscribers to this area: @dotnet/area-extensions-options See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.Issue Details
Description
When adding ApplicationInsights to an ASP.NET core web app's
IServiceCollectionviaAddApplicationInsightsTelemetry()and adding a call toConfigureOptions<ConfigureApplicationInsightsServiceOptions>(), we are experiencing a deadlock.Sample code
Program.cs:
ConfigureApplicationInsightsServiceOptions.cs:
Reproduction Steps
ConfigureOptions<ConfigureApplicationInsightsServiceOptions>()on theIServiceCollectionas shown in the descriptionAddApplicationInsightsTelemetry()on theIServiceCollectionas shown in the descriptionExpected behavior
ConfigureApplicationInsightsServiceOptions.Configure should be invoked when the
WebApplicationBuilderis buildingActual behavior
Deadlock. See running tasks here:
Somehow it is blocking at
runtime/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteRuntimeResolver.cs
Line 84 in 7575716
Regression?
No response
Known Workarounds
No response
Configuration
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.