Skip to content

Dispose per-instance services created by factories#47951

Merged
sharwell merged 9 commits intodotnet:mainfrom
sharwell:dispose-language-services
Jul 19, 2024
Merged

Dispose per-instance services created by factories#47951
sharwell merged 9 commits intodotnet:mainfrom
sharwell:dispose-language-services

Conversation

@sharwell
Copy link
Contributor

@sharwell sharwell commented Sep 22, 2020

This change makes MefWorkspaceServices and MefLanguageServices behave like MEF itself (specifically, its ExportProvider).

  1. (Feature provided by MEF itself) Parts which are exported directly in the MEF catalog and happen to implement IDisposable are disposed when the ExportProvider is disposed. This includes workspace services and language services which are exported directly (as opposed be being exported through a factory)
  2. (Feature provided by this pull request) Parts which are exported through a factory are owned by the workspace that invokes that factory. Any such parts that end up being created and happen to implement IDisposable are disposed automatically when the corresponding workspace is disposed.

The automation of this functionality is primarily valuable because we no longer need to keep track of whether or not a part was created in order to dispose it. Only parts that have already been created are disposed by the workspace.

@sharwell sharwell changed the title Dispose per-instance language services created by factories Dispose per-instance services created by factories Sep 25, 2020
@sharwell sharwell force-pushed the dispose-language-services branch from 22db5a3 to dab1986 Compare September 25, 2020 14:48
@sharwell sharwell marked this pull request as ready for review September 25, 2020 16:42
@sharwell sharwell requested a review from a team as a code owner September 25, 2020 16:42
Copy link
Member

@genlu genlu left a comment

Choose a reason for hiding this comment

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

The change makes sense to me. I'm curious what problem not disposing those Worksapce/Language services causes today.

@sharwell
Copy link
Contributor Author

I'm curious what problem not disposing those Worksapce/Language services causes today.

It leads to direct coupling of Workspace.Dispose every time we need to handle it:

this.Services.GetService<IWorkspaceEventListenerService>()?.Stop();

(_optionService as IWorkspaceOptionService)?.OnWorkspaceDisposed(this);
_optionService.UnregisterWorkspace(this);

There are also two new services that need this support coming in separate pull requests.

@CyrusNajmabadi
Copy link
Contributor

On this PR i'm going to defer to @jasonmalinowski . Primarily because i'm not a fan of IDisposable and/or making more parts of our system have that pattern spread through it. I find it a fairly high mental and design tax to take on that now makes it more difficult for all consumptive uses of these APIs. My preference would strongly be that we only do this as a last resort, and only if there aren't other suitable approaches that can avoid this.

HOwever, i'll defer to @jasonmalinowski here if he feels this is in line with standard patterns and practices for Roslyn, Mef and the greater .net ecosystem. If so, in balance, this is perhaps acceptable given that we would not be forging our own path here and we be consistent with a larger whole.

@sharwell sharwell requested a review from a team as a code owner May 16, 2022 23:30
@sharwell
Copy link
Contributor Author

sharwell commented Mar 3, 2023

@jasonmalinowski @CyrusNajmabadi This is updated and ready to merge.

@CyrusNajmabadi
Copy link
Contributor

I can look on Monday thanks.

@sharwell
Copy link
Contributor Author

sharwell commented Mar 3, 2023

@CyrusNajmabadi note that there are no significant functional changes since the last review from you. Just FYI.

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

This is a nice cleanup for a long-neglected area, so thanks for taking a look at it.

If I have one "concern", I hope we don't have cases where people "helpfully" see the services they might get back from GetService are disposable, and stick it in a using statement and now they're disposing objects underneath other people. Consider any of the disposable lifetime tracking analyzers that might be out there. It could be mitigated by instead introducing something like IDisposableWorkspaceServiceFactory that has a second method that takes the service object (and it's up to the implementer to under the covers call some explicit cleanup method), so that way the service objects themselves aren't disposable. But that's kinda icky though.

Maybe add a remark on GetService that if the object returned is disposable, don't dispose it?

{
internal static class MefUtilities
{
public static void DisposeWithExceptionTracking<T>(T instance, [NotNullIfNotNull("exceptions")] ref List<Exception>? exceptions)
Copy link
Member

Choose a reason for hiding this comment

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

The nullable annotation there on exceptions is intentional and not a tautology?

Copy link
Contributor Author

@sharwell sharwell Mar 5, 2023

Choose a reason for hiding this comment

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

➡️ Without this attribute, the method would be allowed to reassign null to a non-null storage location.

Comment on lines +35 to +36
/// <strong><see cref="IWorkspaceServiceFactory"/> instance constructed externally (e.g. MEF):</strong> Owned by
/// the external source, and will not be automatically disposed when <see cref="Workspace"/> is disposed.
Copy link
Member

Choose a reason for hiding this comment

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

This line confused me a bit, since the IWorkspaceServiceFactory is "definitely" not owned by the workspace, i.e. this bullet might cause more confusion than good.

Copy link
Contributor Author

@sharwell sharwell Mar 5, 2023

Choose a reason for hiding this comment

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

⏱️ I'll see if I can clarify that the only option for IWorkspaceServiceFactory is that it's owned outside the workspace.

Comment on lines +18 to +44
/// <para>Workspace services which implement <see cref="IDisposable"/> are considered ownable, in which case the
/// owner is responsible for disposing of owned instances when they are no longer in use. When
/// <see cref="IWorkspaceService"/> or <see cref="IWorkspaceServiceFactory"/> instances are provided directly to the
/// <see cref="HostWorkspaceServices"/>, the owner of the instances is the type or container (e.g. a MEF export
/// provider) which created the instances. For the specific case of ownable workspace services created by a factory
/// (i.e. instances returned by <see cref="IWorkspaceServiceFactory.CreateService"/>), the <see cref="Workspace"/>
/// is considered the owner of the resulting instance and is expected to be disposed during the call to
/// <see cref="Dispose"/>.</para>
///
/// <para><strong>Summary of lifetime rules</strong></para>
///
/// <list type="bullet">
/// <item><description>
/// <strong><see cref="IWorkspaceService"/> instance constructed externally (e.g. MEF):</strong> Owned by the
/// external source, and will not be automatically disposed when <see cref="Workspace"/> is disposed.
/// </description></item>
/// <item><description>
/// <strong><see cref="IWorkspaceServiceFactory"/> instance constructed externally (e.g. MEF):</strong> Owned by
/// the external source, and will not be automatically disposed when <see cref="Workspace"/> is disposed.
/// </description></item>
/// <item><description>
/// <strong><see cref="IWorkspaceService"/> instance constructed by <see cref="IWorkspaceServiceFactory"/> within
/// the context of <see cref="HostWorkspaceServices"/>:</strong> Owned by <see cref="Workspace"/>, and
/// <strong>will</strong> be automatically disposed when <see cref="Workspace"/> is disposed.
/// </description></item>
/// </list>
/// </remarks>
Copy link
Member

@jasonmalinowski jasonmalinowski Mar 4, 2023

Choose a reason for hiding this comment

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

It feels like this can really be summarized as:

  1. Since this object created them, this object owns disposable IWorkspaceServices that are created from IWorkspaceFactoryServices, and will dispose them.
  2. Workspace services not created from factories and the factories themselves are not disposed.

// Directly dispose IRemoteHostClientProvider if necessary. This is a test hook to ensure RemoteWorkspace
// gets disposed in unit tests as soon as TestWorkspace gets disposed. This would be superseded by direct
// support for IDisposable in https://github.com/dotnet/roslyn/pull/47951.
if (Services.GetService<IRemoteHostClientProvider>() is IDisposable disposableService)
Copy link
Member

Choose a reason for hiding this comment

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

I love the implication here that we'll potentially create the service only to immediately dispose it if it hadn't already been created.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

➡️ Yes, and I tried to call out the benefits of tracking object creation in the top-level pull request comment as well.

Comment on lines +70 to +79
List<Exception> exceptions = null;
foreach (var service in disposableServices)
{
MefUtilities.DisposeWithExceptionTracking(service, ref exceptions);
}

if (exceptions is not null)
{
throw new AggregateException(CompilerExtensionsResources.Instantiated_parts_threw_exceptions_from_IDisposable_Dispose, exceptions);
}
Copy link
Member

Choose a reason for hiding this comment

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

Consider refactoring MefUtilities.DisposeWithExceptionTracking to just do this entire pattern of looping and throwing, since we'll need the same thing in the other place too.

Comment on lines +110 to +129
// MEF workspace service instances created by a factory are not owned by the MEF catalog or disposed
// when the MEF catalog is disposed. Whenever we are potentially going to create an instance of a
// service provided by a factory, we need to check if the resulting service implements IDisposable. The
// specific conditions here are:
//
// * usesFactory: This is true when the workspace service is provided by a factory. Services provided
// directly are owned by the MEF catalog so they do not need to be tracked by the workspace.
// * IsValueCreated: This will be false at least once prior to accessing the lazy value. Once the value
// is known to be created, we no longer need to try adding it to _ownedDisposableServices, so we use a
// lock-free fast path.
var checkAddDisposable = service.usesFactory && !service.lazyService.IsValueCreated;

var serviceInstance = (TWorkspaceService)service.lazyService.Value;
if (checkAddDisposable && serviceInstance is IDisposable disposable)
{
lock (_gate)
{
_ownedDisposableServices.Add(disposable);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Consider extracting and putting this into MefUtilities since it's the same as the other use.

Copy link
Member

Choose a reason for hiding this comment

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

(although I guess it's a strange to pass the lock out...)

@@ -523,13 +522,9 @@ protected virtual void Dispose(bool finalize)

Copy link
Member

Choose a reason for hiding this comment

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

GitHub won't let me leave a comment the line above this, but an easy follow up to this PR is the this.Services.GetService()?.Stop(); line and making that also be disposable based.

Copy link
Contributor Author

@sharwell sharwell Mar 5, 2023

Choose a reason for hiding this comment

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

➡️ This is already part of follow-up #48068.

@sharwell
Copy link
Contributor Author

sharwell commented Mar 5, 2023

If I have one "concern", I hope we don't have cases where people "helpfully" see the services they might get back from GetService are disposable, and stick it in a using statement and now they're disposing objects underneath other people. Consider any of the disposable lifetime tracking analyzers that might be out there. It could be mitigated by instead introducing something like IDisposableWorkspaceServiceFactory that has a second method that takes the service object (and it's up to the implementer to under the covers call some explicit cleanup method), so that way the service objects themselves aren't disposable. But that's kinda icky though.

Typically workspace services are defined with an interface, and exported with an implementation. For disposable service instances, the interface would not inherit IDisposable, but the implementation would. These patterns match the long-standing patterns used by MEF without alteration.

@dotnet dotnet deleted a comment from azure-pipelines bot Sep 12, 2023
@dotnet dotnet deleted a comment from azure-pipelines bot Oct 12, 2023
@dotnet dotnet deleted a comment from azure-pipelines bot Oct 23, 2023
@dotnet dotnet deleted a comment from azure-pipelines bot Oct 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants