Skip to content

Commit e6c9c4d

Browse files
More changes to how we load gladstone extensions. (#77987)
2 parents a2412f3 + e359cc2 commit e6c9c4d

File tree

4 files changed

+125
-138
lines changed

4 files changed

+125
-138
lines changed

src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService.cs

Lines changed: 123 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,26 @@ internal sealed class ExtensionMessageHandlerService(
4747
private readonly SolutionServices _solutionServices = solutionServices;
4848
private readonly IExtensionMessageHandlerFactory _customMessageHandlerFactory = customMessageHandlerFactory;
4949

50-
// Core design: To make things lightweight, and to avoid locking, all work is computed and cached in simple
51-
// immutable dictionaries. These dictionaries are populated on demand, but contain data that can be recomputed
52-
// safely if missing.
50+
/// <summary>
51+
/// Lock for <see cref="_folderPathToExtensionFolder"/>, <see cref="_cachedDocumentHandlers"/>, and <see
52+
/// cref="_cachedWorkspaceHandlers"/>.
53+
/// </summary>
54+
private readonly SemaphoreSlim _gate = new(initialCount: 1);
5355

5456
/// <summary>
5557
/// Extensions assembly load contexts and loaded handlers, indexed by extension folder path.
5658
/// </summary>
57-
private ImmutableDictionary<string, AsyncLazy<IExtensionFolder>> _folderPathToExtensionFolder = ImmutableDictionary<string, AsyncLazy<IExtensionFolder>>.Empty;
59+
private readonly Dictionary<string, AsyncLazy<ExtensionFolder>> _folderPathToExtensionFolder = new();
5860

5961
/// <summary>
6062
/// Cached handlers of document-related messages, indexed by handler message name.
6163
/// </summary>
62-
private ImmutableDictionary<string, AsyncLazy<ImmutableArray<IExtensionMessageHandlerWrapper<Document>>>> _cachedDocumentHandlers = ImmutableDictionary<string, AsyncLazy<ImmutableArray<IExtensionMessageHandlerWrapper<Document>>>>.Empty;
64+
private readonly Dictionary<string, AsyncLazy<ImmutableArray<IExtensionMessageHandlerWrapper<Document>>>> _cachedDocumentHandlers = new();
6365

6466
/// <summary>
6567
/// Cached handlers of non-document-related messages, indexed by handler message name.
6668
/// </summary>
67-
private ImmutableDictionary<string, AsyncLazy<ImmutableArray<IExtensionMessageHandlerWrapper<Solution>>>> _cachedWorkspaceHandlers = ImmutableDictionary<string, AsyncLazy<ImmutableArray<IExtensionMessageHandlerWrapper<Solution>>>>.Empty;
69+
private readonly Dictionary<string, AsyncLazy<ImmutableArray<IExtensionMessageHandlerWrapper<Solution>>>> _cachedWorkspaceHandlers = new();
6870

6971
private async ValueTask<TResult> ExecuteInRemoteOrCurrentProcessAsync<TResult>(
7072
Solution? solution,
@@ -116,18 +118,22 @@ public async ValueTask<RegisterExtensionResponse> RegisterExtensionInCurrentProc
116118
var assemblyFolderPath = Path.GetDirectoryName(assemblyFilePath)
117119
?? throw new InvalidOperationException($"Unable to get the directory name for {assemblyFilePath}.");
118120

119-
var lazy = ImmutableInterlocked.GetOrAdd(
120-
ref _folderPathToExtensionFolder,
121-
assemblyFolderPath,
122-
static (assemblyFolderPath, @this) => AsyncLazy.Create(
123-
cancellationToken => ExtensionFolder.Create(@this, assemblyFolderPath, cancellationToken)),
124-
this);
121+
AsyncLazy<ExtensionFolder> lazyExtensionFolder;
122+
using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
123+
{
124+
lazyExtensionFolder = _folderPathToExtensionFolder.GetOrAdd(
125+
assemblyFolderPath,
126+
static (assemblyFolderPath, @this) => AsyncLazy.Create(
127+
cancellationToken => ExtensionFolder.Create(@this, assemblyFolderPath, cancellationToken)),
128+
this);
125129

126-
var extensionFolder = await lazy.GetValueAsync(cancellationToken).ConfigureAwait(false);
127-
var assemblyHandlers = await extensionFolder.RegisterAssemblyAsync(assemblyFilePath, cancellationToken).ConfigureAwait(false);
130+
// After registering, clear out the cached handler names. They will be recomputed the next time we need them.
131+
ClearCachedHandlers();
132+
}
128133

129-
// After registering, clear out the cached handler names. They will be recomputed the next time we need them.
130-
ClearCachedHandlers();
134+
var extensionFolder = await lazyExtensionFolder.GetValueAsync(cancellationToken).ConfigureAwait(false);
135+
var lazyAssemblyHandlers = extensionFolder.RegisterAssembly(assemblyFilePath);
136+
var assemblyHandlers = await lazyAssemblyHandlers.GetValueAsync(cancellationToken).ConfigureAwait(false);
131137

132138
return new(
133139
[.. assemblyHandlers.WorkspaceMessageHandlers.Keys],
@@ -152,17 +158,23 @@ private async ValueTask<VoidResult> UnregisterExtensionInCurrentProcessAsync(
152158
var assemblyFolderPath = Path.GetDirectoryName(assemblyFilePath)
153159
?? throw new InvalidOperationException($"Unable to get the directory name for {assemblyFilePath}.");
154160

155-
if (_folderPathToExtensionFolder.TryGetValue(assemblyFolderPath, out var lazy))
161+
// Note: unregistering is slightly expensive as we do everything under a lock, to ensure that we have a
162+
// consistent view of the world. This is fine as we don't expect this to be called very often.
163+
using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
156164
{
157-
var extensionFolder = await lazy.GetValueAsync(cancellationToken).ConfigureAwait(false);
158-
// Unregister this particular assembly file from teh assembly folder. If it was the last extension within
159-
// this folder, we can remove the registration for the extension entirely.
160-
if (await extensionFolder.UnregisterAssemblyAsync(assemblyFilePath, cancellationToken).ConfigureAwait(false))
161-
_folderPathToExtensionFolder = _folderPathToExtensionFolder.Remove(assemblyFolderPath);
165+
if (_folderPathToExtensionFolder.TryGetValue(assemblyFolderPath, out var lazyExtensionFolder))
166+
{
167+
var extensionFolder = await lazyExtensionFolder.GetValueAsync(cancellationToken).ConfigureAwait(false);
168+
// Unregister this particular assembly file from teh assembly folder. If it was the last extension within
169+
// this folder, we can remove the registration for the extension entirely.
170+
if (extensionFolder.UnregisterAssembly(assemblyFilePath))
171+
_folderPathToExtensionFolder.Remove(assemblyFolderPath);
172+
}
173+
174+
// After unregistering, clear out the cached handler names. They will be recomputed the next time we need them.
175+
ClearCachedHandlers();
162176
}
163177

164-
// After unregistering, clear out the cached handler names. They will be recomputed the next time we need them.
165-
ClearCachedHandlers();
166178
return default;
167179
}
168180

@@ -177,15 +189,19 @@ await ExecuteInRemoteOrCurrentProcessAsync(
177189

178190
private ValueTask<VoidResult> ResetInCurrentProcessAsync()
179191
{
180-
_folderPathToExtensionFolder = ImmutableDictionary<string, AsyncLazy<IExtensionFolder>>.Empty;
181-
ClearCachedHandlers();
182-
return default;
192+
lock (_gate)
193+
{
194+
_folderPathToExtensionFolder.Clear();
195+
ClearCachedHandlers();
196+
return default;
197+
}
183198
}
184199

185200
private void ClearCachedHandlers()
186201
{
187-
_cachedWorkspaceHandlers = ImmutableDictionary<string, AsyncLazy<ImmutableArray<IExtensionMessageHandlerWrapper<Solution>>>>.Empty;
188-
_cachedDocumentHandlers = ImmutableDictionary<string, AsyncLazy<ImmutableArray<IExtensionMessageHandlerWrapper<Document>>>>.Empty;
202+
Contract.ThrowIfTrue(!Monitor.IsEntered(_gate));
203+
_cachedWorkspaceHandlers.Clear();
204+
_cachedDocumentHandlers.Clear();
189205
}
190206

191207
public async ValueTask<string> HandleExtensionWorkspaceMessageAsync(Solution solution, string messageName, string jsonMessage, CancellationToken cancellationToken)
@@ -210,18 +226,21 @@ public async ValueTask<string> HandleExtensionDocumentMessageAsync(Document docu
210226

211227
private async ValueTask<string> HandleExtensionMessageInCurrentProcessAsync<TArgument>(
212228
TArgument executeArgument, bool isSolution, string messageName, string jsonMessage,
213-
ImmutableDictionary<string, AsyncLazy<ImmutableArray<IExtensionMessageHandlerWrapper<TArgument>>>> cachedHandlers,
229+
Dictionary<string, AsyncLazy<ImmutableArray<IExtensionMessageHandlerWrapper<TArgument>>>> cachedHandlers,
214230
CancellationToken cancellationToken)
215231
{
216-
var lazy = ImmutableInterlocked.GetOrAdd(
217-
ref cachedHandlers,
218-
messageName,
219-
static (messageName, arg) => AsyncLazy.Create(
220-
static (arg, cancellationToken) => ComputeHandlersAsync<TArgument>(arg.@this, arg.messageName, arg.isSolution, cancellationToken),
221-
(messageName, arg.@this, arg.isSolution)),
222-
(@this: this, executeArgument, isSolution));
223-
224-
var handlers = await lazy.GetValueAsync(cancellationToken).ConfigureAwait(false);
232+
AsyncLazy<ImmutableArray<IExtensionMessageHandlerWrapper<TArgument>>> lazyHandlers;
233+
using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
234+
{
235+
lazyHandlers = cachedHandlers.GetOrAdd(
236+
messageName,
237+
static (messageName, arg) => AsyncLazy.Create(
238+
static (arg, cancellationToken) => ComputeHandlersAsync<TArgument>(arg.@this, arg.messageName, arg.isSolution, cancellationToken),
239+
(messageName, arg.@this, arg.isSolution)),
240+
(messageName, @this: this, isSolution));
241+
}
242+
243+
var handlers = await lazyHandlers.GetValueAsync(cancellationToken).ConfigureAwait(false);
225244
if (handlers.Length == 0)
226245
throw new InvalidOperationException($"No handler found for message {messageName}.");
227246

@@ -269,58 +288,62 @@ private static async Task<ImmutableArray<IExtensionMessageHandlerWrapper<TResult
269288
return result.ToImmutable();
270289
}
271290

272-
private interface IExtensionFolder
291+
private abstract class ExtensionFolder
273292
{
274-
ValueTask<AssemblyHandlers> RegisterAssemblyAsync(string assemblyFilePath, CancellationToken cancellationToken);
275-
276293
/// <summary>
277-
/// Unregisters this assembly path from this extension folder. If this was the last registered path, then this
278-
/// will return true so that this folder can be unloaded.
294+
/// Mapping from assembly file path to the handlers it contains. Used as its own lock when mutating.
279295
/// </summary>
280-
ValueTask<bool> UnregisterAssemblyAsync(string assemblyFilePath, CancellationToken cancellationToken);
296+
private readonly Dictionary<string, AsyncLazy<AssemblyHandlers>> _assemblyFilePathToHandlers = new();
281297

282-
ValueTask AddHandlersAsync<TResult>(string messageName, bool isSolution, ArrayBuilder<IExtensionMessageHandlerWrapper<TResult>> result, CancellationToken cancellationToken);
283-
}
298+
protected abstract AssemblyHandlers CreateAssemblyHandlers(string assemblyFilePath, CancellationToken cancellationToken);
284299

285-
/// <summary>
286-
/// Trivial placeholder impl of <see cref="IExtensionFolder"/> when we fail for some reason to even process the
287-
/// folder we are told contains extensions.
288-
/// </summary>
289-
private sealed class TrivialExtensionFolder : IExtensionFolder
290-
{
291-
public static readonly TrivialExtensionFolder Instance = new();
300+
public AsyncLazy<AssemblyHandlers> RegisterAssembly(string assemblyFilePath)
301+
{
302+
lock (_assemblyFilePathToHandlers)
303+
{
304+
return _assemblyFilePathToHandlers.GetOrAdd(
305+
assemblyFilePath,
306+
static (assemblyFilePath, @this) => AsyncLazy.Create(
307+
static (args, cancellationToken) => args.@this.CreateAssemblyHandlers(args.assemblyFilePath, cancellationToken),
308+
(assemblyFilePath, @this)),
309+
this);
310+
}
311+
}
292312

293313
/// <summary>
294-
/// No lock needed as registratin/unregistration must happen serially.
314+
/// Unregisters this assembly path from this extension folder. If this was the last registered path, then this
315+
/// will return true so that this folder can be unloaded.
295316
/// </summary>
296-
private readonly List<string> _registeredFilePaths = [];
297-
298-
public ValueTask<AssemblyHandlers> RegisterAssemblyAsync(string assemblyFilePath, CancellationToken cancellationToken)
317+
public bool UnregisterAssembly(string assemblyFilePath)
299318
{
300-
_registeredFilePaths.Add(assemblyFilePath);
301-
return new(AssemblyHandlers.Empty);
319+
lock (_assemblyFilePathToHandlers)
320+
{
321+
_assemblyFilePathToHandlers.Remove(assemblyFilePath);
322+
return _assemblyFilePathToHandlers.Count == 0;
323+
}
302324
}
303325

304-
public ValueTask<bool> UnregisterAssemblyAsync(string assemblyFilePath, CancellationToken cancellationToken)
326+
public async ValueTask AddHandlersAsync<TResult>(string messageName, bool isSolution, ArrayBuilder<IExtensionMessageHandlerWrapper<TResult>> result, CancellationToken cancellationToken)
305327
{
306-
_registeredFilePaths.Remove(assemblyFilePath);
307-
return new(_registeredFilePaths.Count == 0);
308-
}
309-
310-
public ValueTask AddHandlersAsync<TResult>(string messageName, bool isSolution, ArrayBuilder<IExtensionMessageHandlerWrapper<TResult>> result, CancellationToken cancellationToken)
311-
=> default;
312-
}
313-
314-
private sealed class ExtensionFolder(
315-
ExtensionMessageHandlerService extensionMessageHandlerService,
316-
IAnalyzerAssemblyLoaderInternal analyzerAssemblyLoader) : IExtensionFolder
317-
{
318-
private readonly ExtensionMessageHandlerService _extensionMessageHandlerService = extensionMessageHandlerService;
319-
private readonly IAnalyzerAssemblyLoaderInternal _analyzerAssemblyLoader = analyzerAssemblyLoader;
328+
foreach (var (_, lazyHandlers) in _assemblyFilePathToHandlers)
329+
{
330+
cancellationToken.ThrowIfCancellationRequested();
320331

321-
private ImmutableDictionary<string, AsyncLazy<AssemblyHandlers>> _assemblyFilePathToHandlers = ImmutableDictionary<string, AsyncLazy<AssemblyHandlers>>.Empty;
332+
var handlers = await lazyHandlers.GetValueAsync(cancellationToken).ConfigureAwait(false);
333+
if (isSolution)
334+
{
335+
if (handlers.WorkspaceMessageHandlers.TryGetValue(messageName, out var handler))
336+
result.Add((IExtensionMessageHandlerWrapper<TResult>)handler);
337+
}
338+
else
339+
{
340+
if (handlers.DocumentMessageHandlers.TryGetValue(messageName, out var handler))
341+
result.Add((IExtensionMessageHandlerWrapper<TResult>)handler);
342+
}
343+
}
344+
}
322345

323-
public static IExtensionFolder Create(
346+
public static ExtensionFolder Create(
324347
ExtensionMessageHandlerService extensionMessageHandlerService,
325348
string assemblyFolderPath,
326349
CancellationToken cancellationToken)
@@ -348,36 +371,42 @@ public static IExtensionFolder Create(
348371
analyzerAssemblyLoader.AddDependencyLocation(dll);
349372
}
350373

351-
return new ExtensionFolder(extensionMessageHandlerService, analyzerAssemblyLoader);
374+
return new ShadowCopyExtensionFolder(extensionMessageHandlerService, analyzerAssemblyLoader);
352375
}
353376
catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical))
354377
{
355378
// TODO: Log this exception so the client knows something went wrong.
356379
return new TrivialExtensionFolder();
357380
}
358381
}
382+
}
359383

360-
public async ValueTask<AssemblyHandlers> RegisterAssemblyAsync(
361-
string assemblyFilePath, CancellationToken cancellationToken)
362-
{
363-
var lazy = ImmutableInterlocked.GetOrAdd(
364-
ref _assemblyFilePathToHandlers,
365-
assemblyFilePath,
366-
static (assemblyFilePath, @this) => AsyncLazy.Create(
367-
static (args, cancellationToken) => CreateAssemblyHandlers(args.@this, args.assemblyFilePath, cancellationToken),
368-
(assemblyFilePath, @this)),
369-
this);
384+
/// <summary>
385+
/// Trivial placeholder impl of <see cref="ExtensionFolder"/> when we fail for some reason to even process the
386+
/// folder we are told contains extensions.
387+
/// </summary>
388+
private sealed class TrivialExtensionFolder : ExtensionFolder
389+
{
390+
protected override AssemblyHandlers CreateAssemblyHandlers(string assemblyFilePath, CancellationToken cancellationToken)
391+
=> AssemblyHandlers.Empty;
392+
}
370393

371-
return await lazy.GetValueAsync(cancellationToken).ConfigureAwait(false);
372-
}
394+
/// <summary>
395+
/// Standard impl of <see cref="ExtensionFolder"/> that uses a shadow copy loader to load extensions.
396+
/// </summary>
397+
private sealed class ShadowCopyExtensionFolder(
398+
ExtensionMessageHandlerService extensionMessageHandlerService,
399+
IAnalyzerAssemblyLoaderInternal analyzerAssemblyLoader) : ExtensionFolder
400+
{
401+
private readonly ExtensionMessageHandlerService _extensionMessageHandlerService = extensionMessageHandlerService;
402+
private readonly IAnalyzerAssemblyLoaderInternal _analyzerAssemblyLoader = analyzerAssemblyLoader;
373403

374-
private static AssemblyHandlers CreateAssemblyHandlers(
375-
ExtensionFolder @this, string assemblyFilePath, CancellationToken cancellationToken)
404+
protected override AssemblyHandlers CreateAssemblyHandlers(string assemblyFilePath, CancellationToken cancellationToken)
376405
{
377406
try
378407
{
379-
var assembly = @this._analyzerAssemblyLoader.LoadFromPath(assemblyFilePath);
380-
var factory = @this._extensionMessageHandlerService._customMessageHandlerFactory;
408+
var assembly = _analyzerAssemblyLoader.LoadFromPath(assemblyFilePath);
409+
var factory = _extensionMessageHandlerService._customMessageHandlerFactory;
381410

382411
var messageWorkspaceHandlers = factory
383412
.CreateWorkspaceMessageHandlers(assembly, extensionIdentifier: assemblyFilePath, cancellationToken)
@@ -401,32 +430,6 @@ private static AssemblyHandlers CreateAssemblyHandlers(
401430
return AssemblyHandlers.Empty;
402431
}
403432
}
404-
405-
public async ValueTask AddHandlersAsync<TResult>(string messageName, bool isSolution, ArrayBuilder<IExtensionMessageHandlerWrapper<TResult>> result, CancellationToken cancellationToken)
406-
{
407-
foreach (var (_, lazy) in _assemblyFilePathToHandlers)
408-
{
409-
cancellationToken.ThrowIfCancellationRequested();
410-
411-
var handlers = await lazy.GetValueAsync(cancellationToken).ConfigureAwait(false);
412-
if (isSolution)
413-
{
414-
if (handlers.WorkspaceMessageHandlers.TryGetValue(messageName, out var handler))
415-
result.Add((IExtensionMessageHandlerWrapper<TResult>)handler);
416-
}
417-
else
418-
{
419-
if (handlers.DocumentMessageHandlers.TryGetValue(messageName, out var handler))
420-
result.Add((IExtensionMessageHandlerWrapper<TResult>)handler);
421-
}
422-
}
423-
}
424-
425-
public ValueTask<bool> UnregisterAssemblyAsync(string assemblyFilePath, CancellationToken cancellationToken)
426-
{
427-
_assemblyFilePathToHandlers = _assemblyFilePathToHandlers.Remove(assemblyFilePath);
428-
return new(_assemblyFilePathToHandlers.IsEmpty);
429-
}
430433
}
431434

432435
private sealed class AssemblyHandlers

0 commit comments

Comments
 (0)