Skip to content

Commit 27fdc2d

Browse files
halter73eerhardt
andauthored
Avoid deadlock with ConfigurationManager (#62209)
Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com>
1 parent 2c28e63 commit 27fdc2d

File tree

5 files changed

+387
-67
lines changed

5 files changed

+387
-67
lines changed

src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationManager.cs

Lines changed: 44 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,20 @@ namespace Microsoft.Extensions.Configuration
1414
{
1515
/// <summary>
1616
/// ConfigurationManager is a mutable configuration object. It is both an <see cref="IConfigurationBuilder"/> and an <see cref="IConfigurationRoot"/>.
17-
/// As sources are added, it updates its current view of configuration. Once Build is called, configuration is frozen.
17+
/// As sources are added, it updates its current view of configuration.
1818
/// </summary>
1919
public sealed class ConfigurationManager : IConfigurationBuilder, IConfigurationRoot, IDisposable
2020
{
21+
// Concurrently modifying config sources or properties is not thread-safe. However, it is thread-safe to read config while modifying sources or properties.
2122
private readonly ConfigurationSources _sources;
2223
private readonly ConfigurationBuilderProperties _properties;
2324

24-
private readonly object _providerLock = new();
25-
private readonly List<IConfigurationProvider> _providers = new();
25+
// ReferenceCountedProviderManager manages copy-on-write references to support concurrently reading config while modifying sources.
26+
// It waits for readers to unreference the providers before disposing them without blocking on any concurrent operations.
27+
private readonly ReferenceCountedProviderManager _providerManager = new();
28+
29+
// _changeTokenRegistrations is only modified when config sources are modified. It is not referenced by any read operations.
30+
// Because modify config sources is not thread-safe, modifying _changeTokenRegistrations does not need to be thread-safe either.
2631
private readonly List<IDisposable> _changeTokenRegistrations = new();
2732
private ConfigurationReloadToken _changeToken = new();
2833

@@ -43,55 +48,36 @@ public string? this[string key]
4348
{
4449
get
4550
{
46-
lock (_providerLock)
47-
{
48-
return ConfigurationRoot.GetConfiguration(_providers, key);
49-
}
51+
using ReferenceCountedProviders reference = _providerManager.GetReference();
52+
return ConfigurationRoot.GetConfiguration(reference.Providers, key);
5053
}
5154
set
5255
{
53-
lock (_providerLock)
54-
{
55-
ConfigurationRoot.SetConfiguration(_providers, key, value);
56-
}
56+
using ReferenceCountedProviders reference = _providerManager.GetReference();
57+
ConfigurationRoot.SetConfiguration(reference.Providers, key, value);
5758
}
5859
}
5960

6061
/// <inheritdoc/>
6162
public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key);
6263

6364
/// <inheritdoc/>
64-
public IEnumerable<IConfigurationSection> GetChildren()
65-
{
66-
lock (_providerLock)
67-
{
68-
// ToList() to eagerly evaluate inside lock.
69-
return this.GetChildrenImplementation(null).ToList();
70-
}
71-
}
65+
public IEnumerable<IConfigurationSection> GetChildren() => this.GetChildrenImplementation(null);
7266

7367
IDictionary<string, object> IConfigurationBuilder.Properties => _properties;
7468

7569
IList<IConfigurationSource> IConfigurationBuilder.Sources => _sources;
7670

77-
IEnumerable<IConfigurationProvider> IConfigurationRoot.Providers
78-
{
79-
get
80-
{
81-
lock (_providerLock)
82-
{
83-
return new List<IConfigurationProvider>(_providers);
84-
}
85-
}
86-
}
71+
// We cannot track the duration of the reference to the providers if this property is used.
72+
// If a configuration source is removed after this is accessed but before it's completely enumerated,
73+
// this may allow access to a disposed provider.
74+
IEnumerable<IConfigurationProvider> IConfigurationRoot.Providers => _providerManager.NonReferenceCountedProviders;
8775

8876
/// <inheritdoc/>
8977
public void Dispose()
9078
{
91-
lock (_providerLock)
92-
{
93-
DisposeRegistrationsAndProvidersUnsynchronized();
94-
}
79+
DisposeRegistrations();
80+
_providerManager.Dispose();
9581
}
9682

9783
IConfigurationBuilder IConfigurationBuilder.Add(IConfigurationSource source)
@@ -106,9 +92,9 @@ IConfigurationBuilder IConfigurationBuilder.Add(IConfigurationSource source)
10692

10793
void IConfigurationRoot.Reload()
10894
{
109-
lock (_providerLock)
95+
using (ReferenceCountedProviders reference = _providerManager.GetReference())
11096
{
111-
foreach (var provider in _providers)
97+
foreach (IConfigurationProvider provider in reference.Providers)
11298
{
11399
provider.Load();
114100
}
@@ -117,6 +103,8 @@ void IConfigurationRoot.Reload()
117103
RaiseChanged();
118104
}
119105

106+
internal ReferenceCountedProviders GetProvidersReference() => _providerManager.GetReference();
107+
120108
private void RaiseChanged()
121109
{
122110
var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
@@ -126,59 +114,49 @@ private void RaiseChanged()
126114
// Don't rebuild and reload all providers in the common case when a source is simply added to the IList.
127115
private void AddSource(IConfigurationSource source)
128116
{
129-
lock (_providerLock)
130-
{
131-
var provider = source.Build(this);
132-
_providers.Add(provider);
117+
IConfigurationProvider provider = source.Build(this);
133118

134-
provider.Load();
135-
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => provider.GetReloadToken(), () => RaiseChanged()));
136-
}
119+
provider.Load();
120+
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => provider.GetReloadToken(), () => RaiseChanged()));
137121

122+
_providerManager.AddProvider(provider);
138123
RaiseChanged();
139124
}
140125

141126
// Something other than Add was called on IConfigurationBuilder.Sources or IConfigurationBuilder.Properties has changed.
142127
private void ReloadSources()
143128
{
144-
lock (_providerLock)
145-
{
146-
DisposeRegistrationsAndProvidersUnsynchronized();
129+
DisposeRegistrations();
147130

148-
_changeTokenRegistrations.Clear();
149-
_providers.Clear();
131+
_changeTokenRegistrations.Clear();
150132

151-
foreach (var source in _sources)
152-
{
153-
_providers.Add(source.Build(this));
154-
}
133+
var newProvidersList = new List<IConfigurationProvider>();
155134

156-
foreach (var p in _providers)
157-
{
158-
p.Load();
159-
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
160-
}
135+
foreach (IConfigurationSource source in _sources)
136+
{
137+
newProvidersList.Add(source.Build(this));
138+
}
139+
140+
foreach (IConfigurationProvider p in newProvidersList)
141+
{
142+
p.Load();
143+
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
161144
}
162145

146+
_providerManager.ReplaceProviders(newProvidersList);
163147
RaiseChanged();
164148
}
165149

166-
private void DisposeRegistrationsAndProvidersUnsynchronized()
150+
private void DisposeRegistrations()
167151
{
168152
// dispose change token registrations
169-
foreach (var registration in _changeTokenRegistrations)
153+
foreach (IDisposable registration in _changeTokenRegistrations)
170154
{
171155
registration.Dispose();
172156
}
173-
174-
// dispose providers
175-
foreach (var provider in _providers)
176-
{
177-
(provider as IDisposable)?.Dispose();
178-
}
179157
}
180158

181-
private class ConfigurationSources : IList<IConfigurationSource>
159+
private sealed class ConfigurationSources : IList<IConfigurationSource>
182160
{
183161
private readonly List<IConfigurationSource> _sources = new();
184162
private readonly ConfigurationManager _config;
@@ -259,7 +237,7 @@ IEnumerator IEnumerable.GetEnumerator()
259237
}
260238
}
261239

262-
private class ConfigurationBuilderProperties : IDictionary<string, object>
240+
private sealed class ConfigurationBuilderProperties : IDictionary<string, object>
263241
{
264242
private readonly Dictionary<string, object> _properties = new();
265243
private readonly ConfigurationManager _config;

src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,24 @@ internal static class InternalConfigurationRootExtensions
2020
/// <returns>Immediate children sub-sections of section specified by key.</returns>
2121
internal static IEnumerable<IConfigurationSection> GetChildrenImplementation(this IConfigurationRoot root, string? path)
2222
{
23-
return root.Providers
23+
using ReferenceCountedProviders? reference = (root as ConfigurationManager)?.GetProvidersReference();
24+
IEnumerable<IConfigurationProvider> providers = reference?.Providers ?? root.Providers;
25+
26+
IEnumerable<IConfigurationSection> children = providers
2427
.Aggregate(Enumerable.Empty<string>(),
2528
(seed, source) => source.GetChildKeys(seed, path))
2629
.Distinct(StringComparer.OrdinalIgnoreCase)
2730
.Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key)));
31+
32+
if (reference is null)
33+
{
34+
return children;
35+
}
36+
else
37+
{
38+
// Eagerly evaluate the IEnumerable before releasing the reference so we don't allow iteration over disposed providers.
39+
return children.ToList();
40+
}
2841
}
2942
}
3043
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Threading;
8+
9+
namespace Microsoft.Extensions.Configuration
10+
{
11+
// ReferenceCountedProviders is used by ConfigurationManager to wait until all readers unreference it before disposing any providers.
12+
internal abstract class ReferenceCountedProviders : IDisposable
13+
{
14+
public static ReferenceCountedProviders Create(List<IConfigurationProvider> providers) => new ActiveReferenceCountedProviders(providers);
15+
16+
// If anything references DisposedReferenceCountedProviders, it indicates something is using the ConfigurationManager after it's been disposed.
17+
// We could preemptively throw an ODE from ReferenceCountedProviderManager.GetReference() instead of returning this type, but this might
18+
// break existing apps that are previously able to continue to read configuration after disposing an ConfigurationManager.
19+
public static ReferenceCountedProviders CreateDisposed(List<IConfigurationProvider> providers) => new DisposedReferenceCountedProviders(providers);
20+
21+
public abstract List<IConfigurationProvider> Providers { get; set; }
22+
23+
// NonReferenceCountedProviders is only used to:
24+
// 1. Support IConfigurationRoot.Providers because we cannot track the lifetime of that reference.
25+
// 2. Construct DisposedReferenceCountedProviders because the providers are disposed anyway and no longer reference counted.
26+
public abstract List<IConfigurationProvider> NonReferenceCountedProviders { get; }
27+
28+
public abstract void AddReference();
29+
// This is Dispose() rather than RemoveReference() so we can conveniently release a reference at the end of a using block.
30+
public abstract void Dispose();
31+
32+
private sealed class ActiveReferenceCountedProviders : ReferenceCountedProviders
33+
{
34+
private long _refCount = 1;
35+
// volatile is not strictly necessary because the runtime adds a barrier either way, but volatile indicates that this field has
36+
// unsynchronized readers meaning the all writes initializing the list must be published before updating the _providers reference.
37+
private volatile List<IConfigurationProvider> _providers;
38+
39+
public ActiveReferenceCountedProviders(List<IConfigurationProvider> providers)
40+
{
41+
_providers = providers;
42+
}
43+
44+
public override List<IConfigurationProvider> Providers
45+
{
46+
get
47+
{
48+
Debug.Assert(_refCount > 0);
49+
return _providers;
50+
}
51+
set
52+
{
53+
Debug.Assert(_refCount > 0);
54+
_providers = value;
55+
}
56+
}
57+
58+
public override List<IConfigurationProvider> NonReferenceCountedProviders => _providers;
59+
60+
public override void AddReference()
61+
{
62+
// AddReference() is always called with a lock to ensure _refCount hasn't already decremented to zero.
63+
Debug.Assert(_refCount > 0);
64+
Interlocked.Increment(ref _refCount);
65+
}
66+
67+
public override void Dispose()
68+
{
69+
if (Interlocked.Decrement(ref _refCount) == 0)
70+
{
71+
foreach (IConfigurationProvider provider in _providers)
72+
{
73+
(provider as IDisposable)?.Dispose();
74+
}
75+
}
76+
}
77+
}
78+
79+
private sealed class DisposedReferenceCountedProviders : ReferenceCountedProviders
80+
{
81+
public DisposedReferenceCountedProviders(List<IConfigurationProvider> providers)
82+
{
83+
Providers = providers;
84+
}
85+
86+
public override List<IConfigurationProvider> Providers { get; set; }
87+
public override List<IConfigurationProvider> NonReferenceCountedProviders => Providers;
88+
89+
public override void AddReference() { }
90+
public override void Dispose() { }
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)