Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers - #100641

Closed
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745
Closed

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers#100641
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR fixes#95745 with low risk (to allow backporting), but in an ugly way as the existing public APIs don't allow for a proper fix and introducing a new API would require breaking changes.

// 2) ConfigurationManager is also IDisposable, but it has references both sources and providers.
// When sources change, it creates new providers and disposes the old ones.
// It must not dispose the sources! That is why, in such scenario OwnsFileProvider is set to false.
if (builder is IConfigurationManager)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique. I can share some data offline.

A couple other options to consider:

  1. Just create a new FileProvider if the previous one is disposed. The logic for creating a new one isn't specific to the provider (it's implemented in an extension method we own IIRC).
  2. Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
  3. Implement ref-counting, or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericstj Thank you for feedback! A lot of good questions! Here are my findings:

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

ConfigurationManager can call it more than once. It's called by ReloadSources which is called... anytime a source is being deleted, inserted or any property changes

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique.

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

publicsealedclassConfigurationManager:IConfigurationManager,IConfigurationRoot,IDisposable

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just create a new FileProvider if the previous one is disposed.

I was thinking about it. The idea I had was to null the FileProvider after disposing it and let ReloadSources re-assign it:

publicoverrideIConfigurationProviderBuild(IConfigurationBuilderbuilder)
{
EnsureDefaults(builder);

But then I've realized that ReloadSources creates new providers firsts, then tries to replace (it needs new ones to get rid of old ones) them:

privatevoidReloadSources()
{
DisposeRegistrations();
_changeTokenRegistrations.Clear();
varnewProvidersList=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
newProvidersList.Add(source.Build(this));
}
foreach(IConfigurationProviderpinnewProvidersList)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(p.GetReloadToken,RaiseChanged));
}
_providerManager.ReplaceProviders(newProvidersList);
RaiseChanged();
}

and it tries to do that in thread-safe way:

publicvoidReplaceProviders(List<IConfigurationProvider>providers)
{
ReferenceCountedProvidersoldRefCountedProviders=_refCountedProviders;
lock(_replaceProvidersLock)
{
if(_disposed)
{
thrownewObjectDisposedException(nameof(ConfigurationManager));
}
_refCountedProviders=ReferenceCountedProviders.Create(providers);
}
// Decrement the reference count to the old providers. If they are being concurrently read from
// the actual disposal of the old providers will be delayed until the final reference is released.
// Never dispose ReferenceCountedProviders with a lock because this may call into user code.
oldRefCountedProviders.Dispose();

Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.

This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

Currently a new instance if being allocated if the user has not configured a default one (but I don't know whether for example ASP.NET does not do that somewhere else):

returnGetUserDefinedFileProvider(builder)??newPhysicalFileProvider(AppContext.BaseDirectory??string.Empty);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implement ref-counting

I am not a big fan of ref-counting (it's hardly ever implemented properly and always brings a lot of complexity).

or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today

From my perspective, the main issue is that ConfigurationRoot is given with a list of providers, but it does not have reference to the sources. So it can not dispose the sources with the current public API.

varproviders=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
IConfigurationProviderprovider=source.Build(this);
providers.Add(provider);
}
returnnewConfigurationRoot(providers);

publicvoidDispose()
{
// dispose change token registrations
foreach(IDisposableregistrationin_changeTokenRegistrations)
{
registration.Dispose();
}
// dispose providers
foreach(IConfigurationProviderproviderin_providers)
{
(providerasIDisposable)?.Dispose();
}
}

ConfigurationManager has both and can dispose either one or both of them.

In #100642 I've extended the provider abstraction with a reference to source (most of the implementations already implement a property that returns the source). It allows for disposing what is needed. But introducing a new public API would be a breaking change (so it can not be backported).

@ericstjericstjApr 5, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?
...

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

My point was that special casing ConfigurationManager isn't really a fix. Anyone can call

IConfigurationSourcesource=newDerivedFileConfigurationSource();using(IConfigurationProviderprovider=source.Build(builder)){}using(IConfigurationProviderprovider=source.Build(builder)){}

I believe it's intentional that Sources can produce multiple providers. So we really shouldn't assume that the provider can dispose resources held by the source.

Just create a new FileProvider if the previous one is disposed.
...
Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Maybe you don't need to change the order - if your design is to delegate the lifetime of the FileProvider to the IConfigurationProvider then you should really be creating a new FileProvider for each call to Build. I think this is the solution that's most consistent with the change you made.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

I think it's a tradeoff (but @jozkee might have a better idea as he's our resident FSW expert). I think some systems have expense per-watcher like a background thread and kernel buffers - and those can approach a limit - so more of them uses more resources or could hit the limit - thus the whole polling watcher. From the same token those kernel buffers associated with a watcher limit how many events it can buffer between callbacks - so giving more work to a single one could result in events lost. At least for servicing - let's try to keep the same number of watchers as before. Folks can always set the provider if they want to experiment with a single FSW for perf. If you can see a better option for vNext we can consider that.

Agreed that RefCounting is too big and complex. I think a clear assignment of responsibility of lifetime for the file-provider is what we want. If it's possible to transfer the responsibility to the source and have the source's lifetime managed by something we could do that in the future. I don't think we should make the provider responsible for lifetime of resources held by the source - this regression made it clear that their lifetimes (and instance counts) differ.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping this @adamsitnik

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong disposal of FileConfigurationSource.FileProvider by FileConfigurationProvider.Dispose() on a modification of ConfigurationManager.Sources

2 participants

@adamsitnik@ericstj
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers - #100641

Closed
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745
Closed

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers#100641
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR fixes#95745 with low risk (to allow backporting), but in an ugly way as the existing public APIs don't allow for a proper fix and introducing a new API would require breaking changes.

// 2) ConfigurationManager is also IDisposable, but it has references both sources and providers.
// When sources change, it creates new providers and disposes the old ones.
// It must not dispose the sources! That is why, in such scenario OwnsFileProvider is set to false.
if (builder is IConfigurationManager)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique. I can share some data offline.

A couple other options to consider:

  1. Just create a new FileProvider if the previous one is disposed. The logic for creating a new one isn't specific to the provider (it's implemented in an extension method we own IIRC).
  2. Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
  3. Implement ref-counting, or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericstj Thank you for feedback! A lot of good questions! Here are my findings:

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

ConfigurationManager can call it more than once. It's called by ReloadSources which is called... anytime a source is being deleted, inserted or any property changes

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique.

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

publicsealedclassConfigurationManager:IConfigurationManager,IConfigurationRoot,IDisposable

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just create a new FileProvider if the previous one is disposed.

I was thinking about it. The idea I had was to null the FileProvider after disposing it and let ReloadSources re-assign it:

publicoverrideIConfigurationProviderBuild(IConfigurationBuilderbuilder)
{
EnsureDefaults(builder);

But then I've realized that ReloadSources creates new providers firsts, then tries to replace (it needs new ones to get rid of old ones) them:

privatevoidReloadSources()
{
DisposeRegistrations();
_changeTokenRegistrations.Clear();
varnewProvidersList=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
newProvidersList.Add(source.Build(this));
}
foreach(IConfigurationProviderpinnewProvidersList)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(p.GetReloadToken,RaiseChanged));
}
_providerManager.ReplaceProviders(newProvidersList);
RaiseChanged();
}

and it tries to do that in thread-safe way:

publicvoidReplaceProviders(List<IConfigurationProvider>providers)
{
ReferenceCountedProvidersoldRefCountedProviders=_refCountedProviders;
lock(_replaceProvidersLock)
{
if(_disposed)
{
thrownewObjectDisposedException(nameof(ConfigurationManager));
}
_refCountedProviders=ReferenceCountedProviders.Create(providers);
}
// Decrement the reference count to the old providers. If they are being concurrently read from
// the actual disposal of the old providers will be delayed until the final reference is released.
// Never dispose ReferenceCountedProviders with a lock because this may call into user code.
oldRefCountedProviders.Dispose();

Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.

This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

Currently a new instance if being allocated if the user has not configured a default one (but I don't know whether for example ASP.NET does not do that somewhere else):

returnGetUserDefinedFileProvider(builder)??newPhysicalFileProvider(AppContext.BaseDirectory??string.Empty);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implement ref-counting

I am not a big fan of ref-counting (it's hardly ever implemented properly and always brings a lot of complexity).

or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today

From my perspective, the main issue is that ConfigurationRoot is given with a list of providers, but it does not have reference to the sources. So it can not dispose the sources with the current public API.

varproviders=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
IConfigurationProviderprovider=source.Build(this);
providers.Add(provider);
}
returnnewConfigurationRoot(providers);

publicvoidDispose()
{
// dispose change token registrations
foreach(IDisposableregistrationin_changeTokenRegistrations)
{
registration.Dispose();
}
// dispose providers
foreach(IConfigurationProviderproviderin_providers)
{
(providerasIDisposable)?.Dispose();
}
}

ConfigurationManager has both and can dispose either one or both of them.

In #100642 I've extended the provider abstraction with a reference to source (most of the implementations already implement a property that returns the source). It allows for disposing what is needed. But introducing a new public API would be a breaking change (so it can not be backported).

@ericstjericstjApr 5, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?
...

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

My point was that special casing ConfigurationManager isn't really a fix. Anyone can call

IConfigurationSourcesource=newDerivedFileConfigurationSource();using(IConfigurationProviderprovider=source.Build(builder)){}using(IConfigurationProviderprovider=source.Build(builder)){}

I believe it's intentional that Sources can produce multiple providers. So we really shouldn't assume that the provider can dispose resources held by the source.

Just create a new FileProvider if the previous one is disposed.
...
Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Maybe you don't need to change the order - if your design is to delegate the lifetime of the FileProvider to the IConfigurationProvider then you should really be creating a new FileProvider for each call to Build. I think this is the solution that's most consistent with the change you made.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

I think it's a tradeoff (but @jozkee might have a better idea as he's our resident FSW expert). I think some systems have expense per-watcher like a background thread and kernel buffers - and those can approach a limit - so more of them uses more resources or could hit the limit - thus the whole polling watcher. From the same token those kernel buffers associated with a watcher limit how many events it can buffer between callbacks - so giving more work to a single one could result in events lost. At least for servicing - let's try to keep the same number of watchers as before. Folks can always set the provider if they want to experiment with a single FSW for perf. If you can see a better option for vNext we can consider that.

Agreed that RefCounting is too big and complex. I think a clear assignment of responsibility of lifetime for the file-provider is what we want. If it's possible to transfer the responsibility to the source and have the source's lifetime managed by something we could do that in the future. I don't think we should make the provider responsible for lifetime of resources held by the source - this regression made it clear that their lifetimes (and instance counts) differ.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping this @adamsitnik

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong disposal of FileConfigurationSource.FileProvider by FileConfigurationProvider.Dispose() on a modification of ConfigurationManager.Sources

2 participants

@adamsitnik@ericstj
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers - #100641

Closed
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745
Closed

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers#100641
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR fixes#95745 with low risk (to allow backporting), but in an ugly way as the existing public APIs don't allow for a proper fix and introducing a new API would require breaking changes.

// 2) ConfigurationManager is also IDisposable, but it has references both sources and providers.
// When sources change, it creates new providers and disposes the old ones.
// It must not dispose the sources! That is why, in such scenario OwnsFileProvider is set to false.
if (builder is IConfigurationManager)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique. I can share some data offline.

A couple other options to consider:

  1. Just create a new FileProvider if the previous one is disposed. The logic for creating a new one isn't specific to the provider (it's implemented in an extension method we own IIRC).
  2. Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
  3. Implement ref-counting, or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericstj Thank you for feedback! A lot of good questions! Here are my findings:

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

ConfigurationManager can call it more than once. It's called by ReloadSources which is called... anytime a source is being deleted, inserted or any property changes

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique.

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

publicsealedclassConfigurationManager:IConfigurationManager,IConfigurationRoot,IDisposable

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just create a new FileProvider if the previous one is disposed.

I was thinking about it. The idea I had was to null the FileProvider after disposing it and let ReloadSources re-assign it:

publicoverrideIConfigurationProviderBuild(IConfigurationBuilderbuilder)
{
EnsureDefaults(builder);

But then I've realized that ReloadSources creates new providers firsts, then tries to replace (it needs new ones to get rid of old ones) them:

privatevoidReloadSources()
{
DisposeRegistrations();
_changeTokenRegistrations.Clear();
varnewProvidersList=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
newProvidersList.Add(source.Build(this));
}
foreach(IConfigurationProviderpinnewProvidersList)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(p.GetReloadToken,RaiseChanged));
}
_providerManager.ReplaceProviders(newProvidersList);
RaiseChanged();
}

and it tries to do that in thread-safe way:

publicvoidReplaceProviders(List<IConfigurationProvider>providers)
{
ReferenceCountedProvidersoldRefCountedProviders=_refCountedProviders;
lock(_replaceProvidersLock)
{
if(_disposed)
{
thrownewObjectDisposedException(nameof(ConfigurationManager));
}
_refCountedProviders=ReferenceCountedProviders.Create(providers);
}
// Decrement the reference count to the old providers. If they are being concurrently read from
// the actual disposal of the old providers will be delayed until the final reference is released.
// Never dispose ReferenceCountedProviders with a lock because this may call into user code.
oldRefCountedProviders.Dispose();

Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.

This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

Currently a new instance if being allocated if the user has not configured a default one (but I don't know whether for example ASP.NET does not do that somewhere else):

returnGetUserDefinedFileProvider(builder)??newPhysicalFileProvider(AppContext.BaseDirectory??string.Empty);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implement ref-counting

I am not a big fan of ref-counting (it's hardly ever implemented properly and always brings a lot of complexity).

or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today

From my perspective, the main issue is that ConfigurationRoot is given with a list of providers, but it does not have reference to the sources. So it can not dispose the sources with the current public API.

varproviders=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
IConfigurationProviderprovider=source.Build(this);
providers.Add(provider);
}
returnnewConfigurationRoot(providers);

publicvoidDispose()
{
// dispose change token registrations
foreach(IDisposableregistrationin_changeTokenRegistrations)
{
registration.Dispose();
}
// dispose providers
foreach(IConfigurationProviderproviderin_providers)
{
(providerasIDisposable)?.Dispose();
}
}

ConfigurationManager has both and can dispose either one or both of them.

In #100642 I've extended the provider abstraction with a reference to source (most of the implementations already implement a property that returns the source). It allows for disposing what is needed. But introducing a new public API would be a breaking change (so it can not be backported).

@ericstjericstjApr 5, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?
...

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

My point was that special casing ConfigurationManager isn't really a fix. Anyone can call

IConfigurationSourcesource=newDerivedFileConfigurationSource();using(IConfigurationProviderprovider=source.Build(builder)){}using(IConfigurationProviderprovider=source.Build(builder)){}

I believe it's intentional that Sources can produce multiple providers. So we really shouldn't assume that the provider can dispose resources held by the source.

Just create a new FileProvider if the previous one is disposed.
...
Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Maybe you don't need to change the order - if your design is to delegate the lifetime of the FileProvider to the IConfigurationProvider then you should really be creating a new FileProvider for each call to Build. I think this is the solution that's most consistent with the change you made.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

I think it's a tradeoff (but @jozkee might have a better idea as he's our resident FSW expert). I think some systems have expense per-watcher like a background thread and kernel buffers - and those can approach a limit - so more of them uses more resources or could hit the limit - thus the whole polling watcher. From the same token those kernel buffers associated with a watcher limit how many events it can buffer between callbacks - so giving more work to a single one could result in events lost. At least for servicing - let's try to keep the same number of watchers as before. Folks can always set the provider if they want to experiment with a single FSW for perf. If you can see a better option for vNext we can consider that.

Agreed that RefCounting is too big and complex. I think a clear assignment of responsibility of lifetime for the file-provider is what we want. If it's possible to transfer the responsibility to the source and have the source's lifetime managed by something we could do that in the future. I don't think we should make the provider responsible for lifetime of resources held by the source - this regression made it clear that their lifetimes (and instance counts) differ.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping this @adamsitnik

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong disposal of FileConfigurationSource.FileProvider by FileConfigurationProvider.Dispose() on a modification of ConfigurationManager.Sources

2 participants

@adamsitnik@ericstj
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers - #100641

Closed
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745
Closed

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers#100641
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR fixes#95745 with low risk (to allow backporting), but in an ugly way as the existing public APIs don't allow for a proper fix and introducing a new API would require breaking changes.

// 2) ConfigurationManager is also IDisposable, but it has references both sources and providers.
// When sources change, it creates new providers and disposes the old ones.
// It must not dispose the sources! That is why, in such scenario OwnsFileProvider is set to false.
if (builder is IConfigurationManager)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique. I can share some data offline.

A couple other options to consider:

  1. Just create a new FileProvider if the previous one is disposed. The logic for creating a new one isn't specific to the provider (it's implemented in an extension method we own IIRC).
  2. Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
  3. Implement ref-counting, or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericstj Thank you for feedback! A lot of good questions! Here are my findings:

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

ConfigurationManager can call it more than once. It's called by ReloadSources which is called... anytime a source is being deleted, inserted or any property changes

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique.

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

publicsealedclassConfigurationManager:IConfigurationManager,IConfigurationRoot,IDisposable

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just create a new FileProvider if the previous one is disposed.

I was thinking about it. The idea I had was to null the FileProvider after disposing it and let ReloadSources re-assign it:

publicoverrideIConfigurationProviderBuild(IConfigurationBuilderbuilder)
{
EnsureDefaults(builder);

But then I've realized that ReloadSources creates new providers firsts, then tries to replace (it needs new ones to get rid of old ones) them:

privatevoidReloadSources()
{
DisposeRegistrations();
_changeTokenRegistrations.Clear();
varnewProvidersList=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
newProvidersList.Add(source.Build(this));
}
foreach(IConfigurationProviderpinnewProvidersList)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(p.GetReloadToken,RaiseChanged));
}
_providerManager.ReplaceProviders(newProvidersList);
RaiseChanged();
}

and it tries to do that in thread-safe way:

publicvoidReplaceProviders(List<IConfigurationProvider>providers)
{
ReferenceCountedProvidersoldRefCountedProviders=_refCountedProviders;
lock(_replaceProvidersLock)
{
if(_disposed)
{
thrownewObjectDisposedException(nameof(ConfigurationManager));
}
_refCountedProviders=ReferenceCountedProviders.Create(providers);
}
// Decrement the reference count to the old providers. If they are being concurrently read from
// the actual disposal of the old providers will be delayed until the final reference is released.
// Never dispose ReferenceCountedProviders with a lock because this may call into user code.
oldRefCountedProviders.Dispose();

Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.

This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

Currently a new instance if being allocated if the user has not configured a default one (but I don't know whether for example ASP.NET does not do that somewhere else):

returnGetUserDefinedFileProvider(builder)??newPhysicalFileProvider(AppContext.BaseDirectory??string.Empty);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implement ref-counting

I am not a big fan of ref-counting (it's hardly ever implemented properly and always brings a lot of complexity).

or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today

From my perspective, the main issue is that ConfigurationRoot is given with a list of providers, but it does not have reference to the sources. So it can not dispose the sources with the current public API.

varproviders=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
IConfigurationProviderprovider=source.Build(this);
providers.Add(provider);
}
returnnewConfigurationRoot(providers);

publicvoidDispose()
{
// dispose change token registrations
foreach(IDisposableregistrationin_changeTokenRegistrations)
{
registration.Dispose();
}
// dispose providers
foreach(IConfigurationProviderproviderin_providers)
{
(providerasIDisposable)?.Dispose();
}
}

ConfigurationManager has both and can dispose either one or both of them.

In #100642 I've extended the provider abstraction with a reference to source (most of the implementations already implement a property that returns the source). It allows for disposing what is needed. But introducing a new public API would be a breaking change (so it can not be backported).

@ericstjericstjApr 5, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?
...

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

My point was that special casing ConfigurationManager isn't really a fix. Anyone can call

IConfigurationSourcesource=newDerivedFileConfigurationSource();using(IConfigurationProviderprovider=source.Build(builder)){}using(IConfigurationProviderprovider=source.Build(builder)){}

I believe it's intentional that Sources can produce multiple providers. So we really shouldn't assume that the provider can dispose resources held by the source.

Just create a new FileProvider if the previous one is disposed.
...
Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Maybe you don't need to change the order - if your design is to delegate the lifetime of the FileProvider to the IConfigurationProvider then you should really be creating a new FileProvider for each call to Build. I think this is the solution that's most consistent with the change you made.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

I think it's a tradeoff (but @jozkee might have a better idea as he's our resident FSW expert). I think some systems have expense per-watcher like a background thread and kernel buffers - and those can approach a limit - so more of them uses more resources or could hit the limit - thus the whole polling watcher. From the same token those kernel buffers associated with a watcher limit how many events it can buffer between callbacks - so giving more work to a single one could result in events lost. At least for servicing - let's try to keep the same number of watchers as before. Folks can always set the provider if they want to experiment with a single FSW for perf. If you can see a better option for vNext we can consider that.

Agreed that RefCounting is too big and complex. I think a clear assignment of responsibility of lifetime for the file-provider is what we want. If it's possible to transfer the responsibility to the source and have the source's lifetime managed by something we could do that in the future. I don't think we should make the provider responsible for lifetime of resources held by the source - this regression made it clear that their lifetimes (and instance counts) differ.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping this @adamsitnik

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong disposal of FileConfigurationSource.FileProvider by FileConfigurationProvider.Dispose() on a modification of ConfigurationManager.Sources

2 participants

@adamsitnik@ericstj
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers - #100641

Closed
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745
Closed

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers#100641
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR fixes#95745 with low risk (to allow backporting), but in an ugly way as the existing public APIs don't allow for a proper fix and introducing a new API would require breaking changes.

// 2) ConfigurationManager is also IDisposable, but it has references both sources and providers.
// When sources change, it creates new providers and disposes the old ones.
// It must not dispose the sources! That is why, in such scenario OwnsFileProvider is set to false.
if (builder is IConfigurationManager)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique. I can share some data offline.

A couple other options to consider:

  1. Just create a new FileProvider if the previous one is disposed. The logic for creating a new one isn't specific to the provider (it's implemented in an extension method we own IIRC).
  2. Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
  3. Implement ref-counting, or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericstj Thank you for feedback! A lot of good questions! Here are my findings:

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

ConfigurationManager can call it more than once. It's called by ReloadSources which is called... anytime a source is being deleted, inserted or any property changes

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique.

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

publicsealedclassConfigurationManager:IConfigurationManager,IConfigurationRoot,IDisposable

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just create a new FileProvider if the previous one is disposed.

I was thinking about it. The idea I had was to null the FileProvider after disposing it and let ReloadSources re-assign it:

publicoverrideIConfigurationProviderBuild(IConfigurationBuilderbuilder)
{
EnsureDefaults(builder);

But then I've realized that ReloadSources creates new providers firsts, then tries to replace (it needs new ones to get rid of old ones) them:

privatevoidReloadSources()
{
DisposeRegistrations();
_changeTokenRegistrations.Clear();
varnewProvidersList=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
newProvidersList.Add(source.Build(this));
}
foreach(IConfigurationProviderpinnewProvidersList)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(p.GetReloadToken,RaiseChanged));
}
_providerManager.ReplaceProviders(newProvidersList);
RaiseChanged();
}

and it tries to do that in thread-safe way:

publicvoidReplaceProviders(List<IConfigurationProvider>providers)
{
ReferenceCountedProvidersoldRefCountedProviders=_refCountedProviders;
lock(_replaceProvidersLock)
{
if(_disposed)
{
thrownewObjectDisposedException(nameof(ConfigurationManager));
}
_refCountedProviders=ReferenceCountedProviders.Create(providers);
}
// Decrement the reference count to the old providers. If they are being concurrently read from
// the actual disposal of the old providers will be delayed until the final reference is released.
// Never dispose ReferenceCountedProviders with a lock because this may call into user code.
oldRefCountedProviders.Dispose();

Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.

This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

Currently a new instance if being allocated if the user has not configured a default one (but I don't know whether for example ASP.NET does not do that somewhere else):

returnGetUserDefinedFileProvider(builder)??newPhysicalFileProvider(AppContext.BaseDirectory??string.Empty);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implement ref-counting

I am not a big fan of ref-counting (it's hardly ever implemented properly and always brings a lot of complexity).

or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today

From my perspective, the main issue is that ConfigurationRoot is given with a list of providers, but it does not have reference to the sources. So it can not dispose the sources with the current public API.

varproviders=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
IConfigurationProviderprovider=source.Build(this);
providers.Add(provider);
}
returnnewConfigurationRoot(providers);

publicvoidDispose()
{
// dispose change token registrations
foreach(IDisposableregistrationin_changeTokenRegistrations)
{
registration.Dispose();
}
// dispose providers
foreach(IConfigurationProviderproviderin_providers)
{
(providerasIDisposable)?.Dispose();
}
}

ConfigurationManager has both and can dispose either one or both of them.

In #100642 I've extended the provider abstraction with a reference to source (most of the implementations already implement a property that returns the source). It allows for disposing what is needed. But introducing a new public API would be a breaking change (so it can not be backported).

@ericstjericstjApr 5, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?
...

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

My point was that special casing ConfigurationManager isn't really a fix. Anyone can call

IConfigurationSourcesource=newDerivedFileConfigurationSource();using(IConfigurationProviderprovider=source.Build(builder)){}using(IConfigurationProviderprovider=source.Build(builder)){}

I believe it's intentional that Sources can produce multiple providers. So we really shouldn't assume that the provider can dispose resources held by the source.

Just create a new FileProvider if the previous one is disposed.
...
Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Maybe you don't need to change the order - if your design is to delegate the lifetime of the FileProvider to the IConfigurationProvider then you should really be creating a new FileProvider for each call to Build. I think this is the solution that's most consistent with the change you made.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

I think it's a tradeoff (but @jozkee might have a better idea as he's our resident FSW expert). I think some systems have expense per-watcher like a background thread and kernel buffers - and those can approach a limit - so more of them uses more resources or could hit the limit - thus the whole polling watcher. From the same token those kernel buffers associated with a watcher limit how many events it can buffer between callbacks - so giving more work to a single one could result in events lost. At least for servicing - let's try to keep the same number of watchers as before. Folks can always set the provider if they want to experiment with a single FSW for perf. If you can see a better option for vNext we can consider that.

Agreed that RefCounting is too big and complex. I think a clear assignment of responsibility of lifetime for the file-provider is what we want. If it's possible to transfer the responsibility to the source and have the source's lifetime managed by something we could do that in the future. I don't think we should make the provider responsible for lifetime of resources held by the source - this regression made it clear that their lifetimes (and instance counts) differ.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping this @adamsitnik

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong disposal of FileConfigurationSource.FileProvider by FileConfigurationProvider.Dispose() on a modification of ConfigurationManager.Sources

2 participants

@adamsitnik@ericstj
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers - #100641

Closed
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745
Closed

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers#100641
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR fixes#95745 with low risk (to allow backporting), but in an ugly way as the existing public APIs don't allow for a proper fix and introducing a new API would require breaking changes.

// 2) ConfigurationManager is also IDisposable, but it has references both sources and providers.
// When sources change, it creates new providers and disposes the old ones.
// It must not dispose the sources! That is why, in such scenario OwnsFileProvider is set to false.
if (builder is IConfigurationManager)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique. I can share some data offline.

A couple other options to consider:

  1. Just create a new FileProvider if the previous one is disposed. The logic for creating a new one isn't specific to the provider (it's implemented in an extension method we own IIRC).
  2. Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
  3. Implement ref-counting, or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericstj Thank you for feedback! A lot of good questions! Here are my findings:

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

ConfigurationManager can call it more than once. It's called by ReloadSources which is called... anytime a source is being deleted, inserted or any property changes

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique.

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

publicsealedclassConfigurationManager:IConfigurationManager,IConfigurationRoot,IDisposable

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just create a new FileProvider if the previous one is disposed.

I was thinking about it. The idea I had was to null the FileProvider after disposing it and let ReloadSources re-assign it:

publicoverrideIConfigurationProviderBuild(IConfigurationBuilderbuilder)
{
EnsureDefaults(builder);

But then I've realized that ReloadSources creates new providers firsts, then tries to replace (it needs new ones to get rid of old ones) them:

privatevoidReloadSources()
{
DisposeRegistrations();
_changeTokenRegistrations.Clear();
varnewProvidersList=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
newProvidersList.Add(source.Build(this));
}
foreach(IConfigurationProviderpinnewProvidersList)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(p.GetReloadToken,RaiseChanged));
}
_providerManager.ReplaceProviders(newProvidersList);
RaiseChanged();
}

and it tries to do that in thread-safe way:

publicvoidReplaceProviders(List<IConfigurationProvider>providers)
{
ReferenceCountedProvidersoldRefCountedProviders=_refCountedProviders;
lock(_replaceProvidersLock)
{
if(_disposed)
{
thrownewObjectDisposedException(nameof(ConfigurationManager));
}
_refCountedProviders=ReferenceCountedProviders.Create(providers);
}
// Decrement the reference count to the old providers. If they are being concurrently read from
// the actual disposal of the old providers will be delayed until the final reference is released.
// Never dispose ReferenceCountedProviders with a lock because this may call into user code.
oldRefCountedProviders.Dispose();

Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.

This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

Currently a new instance if being allocated if the user has not configured a default one (but I don't know whether for example ASP.NET does not do that somewhere else):

returnGetUserDefinedFileProvider(builder)??newPhysicalFileProvider(AppContext.BaseDirectory??string.Empty);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implement ref-counting

I am not a big fan of ref-counting (it's hardly ever implemented properly and always brings a lot of complexity).

or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today

From my perspective, the main issue is that ConfigurationRoot is given with a list of providers, but it does not have reference to the sources. So it can not dispose the sources with the current public API.

varproviders=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
IConfigurationProviderprovider=source.Build(this);
providers.Add(provider);
}
returnnewConfigurationRoot(providers);

publicvoidDispose()
{
// dispose change token registrations
foreach(IDisposableregistrationin_changeTokenRegistrations)
{
registration.Dispose();
}
// dispose providers
foreach(IConfigurationProviderproviderin_providers)
{
(providerasIDisposable)?.Dispose();
}
}

ConfigurationManager has both and can dispose either one or both of them.

In #100642 I've extended the provider abstraction with a reference to source (most of the implementations already implement a property that returns the source). It allows for disposing what is needed. But introducing a new public API would be a breaking change (so it can not be backported).

@ericstjericstjApr 5, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?
...

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

My point was that special casing ConfigurationManager isn't really a fix. Anyone can call

IConfigurationSourcesource=newDerivedFileConfigurationSource();using(IConfigurationProviderprovider=source.Build(builder)){}using(IConfigurationProviderprovider=source.Build(builder)){}

I believe it's intentional that Sources can produce multiple providers. So we really shouldn't assume that the provider can dispose resources held by the source.

Just create a new FileProvider if the previous one is disposed.
...
Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Maybe you don't need to change the order - if your design is to delegate the lifetime of the FileProvider to the IConfigurationProvider then you should really be creating a new FileProvider for each call to Build. I think this is the solution that's most consistent with the change you made.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

I think it's a tradeoff (but @jozkee might have a better idea as he's our resident FSW expert). I think some systems have expense per-watcher like a background thread and kernel buffers - and those can approach a limit - so more of them uses more resources or could hit the limit - thus the whole polling watcher. From the same token those kernel buffers associated with a watcher limit how many events it can buffer between callbacks - so giving more work to a single one could result in events lost. At least for servicing - let's try to keep the same number of watchers as before. Folks can always set the provider if they want to experiment with a single FSW for perf. If you can see a better option for vNext we can consider that.

Agreed that RefCounting is too big and complex. I think a clear assignment of responsibility of lifetime for the file-provider is what we want. If it's possible to transfer the responsibility to the source and have the source's lifetime managed by something we could do that in the future. I don't think we should make the provider responsible for lifetime of resources held by the source - this regression made it clear that their lifetimes (and instance counts) differ.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping this @adamsitnik

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong disposal of FileConfigurationSource.FileProvider by FileConfigurationProvider.Dispose() on a modification of ConfigurationManager.Sources

2 participants

@adamsitnik@ericstj
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers - #100641

Closed
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745
Closed

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers#100641
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR fixes#95745 with low risk (to allow backporting), but in an ugly way as the existing public APIs don't allow for a proper fix and introducing a new API would require breaking changes.

// 2) ConfigurationManager is also IDisposable, but it has references both sources and providers.
// When sources change, it creates new providers and disposes the old ones.
// It must not dispose the sources! That is why, in such scenario OwnsFileProvider is set to false.
if (builder is IConfigurationManager)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique. I can share some data offline.

A couple other options to consider:

  1. Just create a new FileProvider if the previous one is disposed. The logic for creating a new one isn't specific to the provider (it's implemented in an extension method we own IIRC).
  2. Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
  3. Implement ref-counting, or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericstj Thank you for feedback! A lot of good questions! Here are my findings:

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

ConfigurationManager can call it more than once. It's called by ReloadSources which is called... anytime a source is being deleted, inserted or any property changes

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique.

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

publicsealedclassConfigurationManager:IConfigurationManager,IConfigurationRoot,IDisposable

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just create a new FileProvider if the previous one is disposed.

I was thinking about it. The idea I had was to null the FileProvider after disposing it and let ReloadSources re-assign it:

publicoverrideIConfigurationProviderBuild(IConfigurationBuilderbuilder)
{
EnsureDefaults(builder);

But then I've realized that ReloadSources creates new providers firsts, then tries to replace (it needs new ones to get rid of old ones) them:

privatevoidReloadSources()
{
DisposeRegistrations();
_changeTokenRegistrations.Clear();
varnewProvidersList=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
newProvidersList.Add(source.Build(this));
}
foreach(IConfigurationProviderpinnewProvidersList)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(p.GetReloadToken,RaiseChanged));
}
_providerManager.ReplaceProviders(newProvidersList);
RaiseChanged();
}

and it tries to do that in thread-safe way:

publicvoidReplaceProviders(List<IConfigurationProvider>providers)
{
ReferenceCountedProvidersoldRefCountedProviders=_refCountedProviders;
lock(_replaceProvidersLock)
{
if(_disposed)
{
thrownewObjectDisposedException(nameof(ConfigurationManager));
}
_refCountedProviders=ReferenceCountedProviders.Create(providers);
}
// Decrement the reference count to the old providers. If they are being concurrently read from
// the actual disposal of the old providers will be delayed until the final reference is released.
// Never dispose ReferenceCountedProviders with a lock because this may call into user code.
oldRefCountedProviders.Dispose();

Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.

This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

Currently a new instance if being allocated if the user has not configured a default one (but I don't know whether for example ASP.NET does not do that somewhere else):

returnGetUserDefinedFileProvider(builder)??newPhysicalFileProvider(AppContext.BaseDirectory??string.Empty);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implement ref-counting

I am not a big fan of ref-counting (it's hardly ever implemented properly and always brings a lot of complexity).

or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today

From my perspective, the main issue is that ConfigurationRoot is given with a list of providers, but it does not have reference to the sources. So it can not dispose the sources with the current public API.

varproviders=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
IConfigurationProviderprovider=source.Build(this);
providers.Add(provider);
}
returnnewConfigurationRoot(providers);

publicvoidDispose()
{
// dispose change token registrations
foreach(IDisposableregistrationin_changeTokenRegistrations)
{
registration.Dispose();
}
// dispose providers
foreach(IConfigurationProviderproviderin_providers)
{
(providerasIDisposable)?.Dispose();
}
}

ConfigurationManager has both and can dispose either one or both of them.

In #100642 I've extended the provider abstraction with a reference to source (most of the implementations already implement a property that returns the source). It allows for disposing what is needed. But introducing a new public API would be a breaking change (so it can not be backported).

@ericstjericstjApr 5, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?
...

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

My point was that special casing ConfigurationManager isn't really a fix. Anyone can call

IConfigurationSourcesource=newDerivedFileConfigurationSource();using(IConfigurationProviderprovider=source.Build(builder)){}using(IConfigurationProviderprovider=source.Build(builder)){}

I believe it's intentional that Sources can produce multiple providers. So we really shouldn't assume that the provider can dispose resources held by the source.

Just create a new FileProvider if the previous one is disposed.
...
Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Maybe you don't need to change the order - if your design is to delegate the lifetime of the FileProvider to the IConfigurationProvider then you should really be creating a new FileProvider for each call to Build. I think this is the solution that's most consistent with the change you made.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

I think it's a tradeoff (but @jozkee might have a better idea as he's our resident FSW expert). I think some systems have expense per-watcher like a background thread and kernel buffers - and those can approach a limit - so more of them uses more resources or could hit the limit - thus the whole polling watcher. From the same token those kernel buffers associated with a watcher limit how many events it can buffer between callbacks - so giving more work to a single one could result in events lost. At least for servicing - let's try to keep the same number of watchers as before. Folks can always set the provider if they want to experiment with a single FSW for perf. If you can see a better option for vNext we can consider that.

Agreed that RefCounting is too big and complex. I think a clear assignment of responsibility of lifetime for the file-provider is what we want. If it's possible to transfer the responsibility to the source and have the source's lifetime managed by something we could do that in the future. I don't think we should make the provider responsible for lifetime of resources held by the source - this regression made it clear that their lifetimes (and instance counts) differ.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping this @adamsitnik

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong disposal of FileConfigurationSource.FileProvider by FileConfigurationProvider.Dispose() on a modification of ConfigurationManager.Sources

2 participants

@adamsitnik@ericstj
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers - #100641

Closed
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745
Closed

Reloading sources in ConfigurationManager should not dispose IConfigurationSources, only providers#100641
adamsitnik wants to merge 2 commits into
dotnet:mainfrom
adamsitnik:issue95745

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR fixes#95745 with low risk (to allow backporting), but in an ugly way as the existing public APIs don't allow for a proper fix and introducing a new API would require breaking changes.

// 2) ConfigurationManager is also IDisposable, but it has references both sources and providers.
// When sources change, it creates new providers and disposes the old ones.
// It must not dispose the sources! That is why, in such scenario OwnsFileProvider is set to false.
if (builder is IConfigurationManager)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique. I can share some data offline.

A couple other options to consider:

  1. Just create a new FileProvider if the previous one is disposed. The logic for creating a new one isn't specific to the provider (it's implemented in an extension method we own IIRC).
  2. Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
  3. Implement ref-counting, or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericstj Thank you for feedback! A lot of good questions! Here are my findings:

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?

ConfigurationManager can call it more than once. It's called by ReloadSources which is called... anytime a source is being deleted, inserted or any property changes

Also, I've noticed others implement IConfigurationBuilder before. Some are forks of ConfigurationManager, others are unique.

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

publicsealedclassConfigurationManager:IConfigurationManager,IConfigurationRoot,IDisposable

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just create a new FileProvider if the previous one is disposed.

I was thinking about it. The idea I had was to null the FileProvider after disposing it and let ReloadSources re-assign it:

publicoverrideIConfigurationProviderBuild(IConfigurationBuilderbuilder)
{
EnsureDefaults(builder);

But then I've realized that ReloadSources creates new providers firsts, then tries to replace (it needs new ones to get rid of old ones) them:

privatevoidReloadSources()
{
DisposeRegistrations();
_changeTokenRegistrations.Clear();
varnewProvidersList=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
newProvidersList.Add(source.Build(this));
}
foreach(IConfigurationProviderpinnewProvidersList)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(p.GetReloadToken,RaiseChanged));
}
_providerManager.ReplaceProviders(newProvidersList);
RaiseChanged();
}

and it tries to do that in thread-safe way:

publicvoidReplaceProviders(List<IConfigurationProvider>providers)
{
ReferenceCountedProvidersoldRefCountedProviders=_refCountedProviders;
lock(_replaceProvidersLock)
{
if(_disposed)
{
thrownewObjectDisposedException(nameof(ConfigurationManager));
}
_refCountedProviders=ReferenceCountedProviders.Create(providers);
}
// Decrement the reference count to the old providers. If they are being concurrently read from
// the actual disposal of the old providers will be delayed until the final reference is released.
// Never dispose ReferenceCountedProviders with a lock because this may call into user code.
oldRefCountedProviders.Dispose();

Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.

This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

Currently a new instance if being allocated if the user has not configured a default one (but I don't know whether for example ASP.NET does not do that somewhere else):

returnGetUserDefinedFileProvider(builder)??newPhysicalFileProvider(AppContext.BaseDirectory??string.Empty);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implement ref-counting

I am not a big fan of ref-counting (it's hardly ever implemented properly and always brings a lot of complexity).

or disposal pattern for the source. If we just add IDispose to the source impl, will it get called by whichever component manages it's lifetime today

From my perspective, the main issue is that ConfigurationRoot is given with a list of providers, but it does not have reference to the sources. So it can not dispose the sources with the current public API.

varproviders=newList<IConfigurationProvider>();
foreach(IConfigurationSourcesourcein_sources)
{
IConfigurationProviderprovider=source.Build(this);
providers.Add(provider);
}
returnnewConfigurationRoot(providers);

publicvoidDispose()
{
// dispose change token registrations
foreach(IDisposableregistrationin_changeTokenRegistrations)
{
registration.Dispose();
}
// dispose providers
foreach(IConfigurationProviderproviderin_providers)
{
(providerasIDisposable)?.Dispose();
}
}

ConfigurationManager has both and can dispose either one or both of them.

In #100642 I've extended the provider abstraction with a reference to source (most of the implementations already implement a property that returns the source). It allows for disposing what is needed. But introducing a new public API would be a breaking change (so it can not be backported).

@ericstjericstjApr 5, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a safe assumption? Can't any FileConfigurationSource have Build called more than once?
...

ConfigurationManager is sealed, I could change this condition from to check for explicit type.

My point was that special casing ConfigurationManager isn't really a fix. Anyone can call

IConfigurationSourcesource=newDerivedFileConfigurationSource();using(IConfigurationProviderprovider=source.Build(builder)){}using(IConfigurationProviderprovider=source.Build(builder)){}

I believe it's intentional that Sources can produce multiple providers. So we really shouldn't assume that the provider can dispose resources held by the source.

Just create a new FileProvider if the previous one is disposed.
...
Changing the order would most likely be possible, but I don't see a possibility to do it in a simple way with low risk (I was searching for a fix that could be backported to 8.0)

Maybe you don't need to change the order - if your design is to delegate the lifetime of the FileProvider to the IConfigurationProvider then you should really be creating a new FileProvider for each call to Build. I think this is the solution that's most consistent with the change you made.

Share a single FIleProvider for default and don't dispose it. We still leak until finalization, but only one is leaked.
This is something I've not considered. How would it affect perf on various OSes? Is it better to have a single instance of PhysicalFilesWatcher rather than many?

I think it's a tradeoff (but @jozkee might have a better idea as he's our resident FSW expert). I think some systems have expense per-watcher like a background thread and kernel buffers - and those can approach a limit - so more of them uses more resources or could hit the limit - thus the whole polling watcher. From the same token those kernel buffers associated with a watcher limit how many events it can buffer between callbacks - so giving more work to a single one could result in events lost. At least for servicing - let's try to keep the same number of watchers as before. Folks can always set the provider if they want to experiment with a single FSW for perf. If you can see a better option for vNext we can consider that.

Agreed that RefCounting is too big and complex. I think a clear assignment of responsibility of lifetime for the file-provider is what we want. If it's possible to transfer the responsibility to the source and have the source's lifetime managed by something we could do that in the future. I don't think we should make the provider responsible for lifetime of resources held by the source - this regression made it clear that their lifetimes (and instance counts) differ.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping this @adamsitnik

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong disposal of FileConfigurationSource.FileProvider by FileConfigurationProvider.Dispose() on a modification of ConfigurationManager.Sources

2 participants

@adamsitnik@ericstj