@@ -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 < IExtensionFolder > > _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 < IExtensionFolder > 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,21 @@ 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+ using ( await _gate . DisposableWaitAsync ( cancellationToken ) . ConfigureAwait ( false ) )
156162 {
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 ) ;
163+ if ( _folderPathToExtensionFolder . TryGetValue ( assemblyFolderPath , out var lazyExtensionFolder ) )
164+ {
165+ var extensionFolder = await lazyExtensionFolder . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
166+ // Unregister this particular assembly file from teh assembly folder. If it was the last extension within
167+ // this folder, we can remove the registration for the extension entirely.
168+ if ( extensionFolder . UnregisterAssembly ( assemblyFilePath ) )
169+ _folderPathToExtensionFolder . Remove ( assemblyFolderPath ) ;
170+ }
171+
172+ // After unregistering, clear out the cached handler names. They will be recomputed the next time we need them.
173+ ClearCachedHandlers ( ) ;
162174 }
163175
164- // After unregistering, clear out the cached handler names. They will be recomputed the next time we need them.
165- ClearCachedHandlers ( ) ;
166176 return default ;
167177 }
168178
@@ -177,15 +187,19 @@ await ExecuteInRemoteOrCurrentProcessAsync(
177187
178188 private ValueTask < VoidResult > ResetInCurrentProcessAsync ( )
179189 {
180- _folderPathToExtensionFolder = ImmutableDictionary < string , AsyncLazy < IExtensionFolder > > . Empty ;
181- ClearCachedHandlers ( ) ;
182- return default ;
190+ lock ( _gate )
191+ {
192+ _folderPathToExtensionFolder . Clear ( ) ;
193+ ClearCachedHandlers ( ) ;
194+ return default ;
195+ }
183196 }
184197
185198 private void ClearCachedHandlers ( )
186199 {
187- _cachedWorkspaceHandlers = ImmutableDictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < Solution > > > > . Empty ;
188- _cachedDocumentHandlers = ImmutableDictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < Document > > > > . Empty ;
200+ Contract . ThrowIfTrue ( ! Monitor . IsEntered ( _gate ) ) ;
201+ _cachedWorkspaceHandlers . Clear ( ) ;
202+ _cachedDocumentHandlers . Clear ( ) ;
189203 }
190204
191205 public async ValueTask < string > HandleExtensionWorkspaceMessageAsync ( Solution solution , string messageName , string jsonMessage , CancellationToken cancellationToken )
@@ -210,18 +224,21 @@ public async ValueTask<string> HandleExtensionDocumentMessageAsync(Document docu
210224
211225 private async ValueTask < string > HandleExtensionMessageInCurrentProcessAsync < TArgument > (
212226 TArgument executeArgument , bool isSolution , string messageName , string jsonMessage ,
213- ImmutableDictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < TArgument > > > > cachedHandlers ,
227+ Dictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < TArgument > > > > cachedHandlers ,
214228 CancellationToken cancellationToken )
215229 {
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 ) ;
230+ AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < TArgument > > > lazyHandlers ;
231+ using ( await _gate . DisposableWaitAsync ( cancellationToken ) . ConfigureAwait ( false ) )
232+ {
233+ lazyHandlers = cachedHandlers . GetOrAdd (
234+ messageName ,
235+ static ( messageName , arg ) => AsyncLazy . Create (
236+ static ( arg , cancellationToken ) => ComputeHandlersAsync < TArgument > ( arg . @this , arg . messageName , arg . isSolution , cancellationToken ) ,
237+ ( messageName , arg . @this , arg . isSolution ) ) ,
238+ ( messageName , @this : this , isSolution ) ) ;
239+ }
240+
241+ var handlers = await lazyHandlers . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
225242 if ( handlers . Length == 0 )
226243 throw new InvalidOperationException ( $ "No handler found for message { messageName } .") ;
227244
@@ -271,13 +288,13 @@ private static async Task<ImmutableArray<IExtensionMessageHandlerWrapper<TResult
271288
272289 private interface IExtensionFolder
273290 {
274- ValueTask < AssemblyHandlers > RegisterAssemblyAsync ( string assemblyFilePath , CancellationToken cancellationToken ) ;
291+ AsyncLazy < AssemblyHandlers > RegisterAssembly ( string assemblyFilePath ) ;
275292
276293 /// <summary>
277294 /// Unregisters this assembly path from this extension folder. If this was the last registered path, then this
278295 /// will return true so that this folder can be unloaded.
279296 /// </summary>
280- ValueTask < bool > UnregisterAssemblyAsync ( string assemblyFilePath , CancellationToken cancellationToken ) ;
297+ bool UnregisterAssembly ( string assemblyFilePath ) ;
281298
282299 ValueTask AddHandlersAsync < TResult > ( string messageName , bool isSolution , ArrayBuilder < IExtensionMessageHandlerWrapper < TResult > > result , CancellationToken cancellationToken ) ;
283300 }
@@ -291,20 +308,20 @@ private sealed class TrivialExtensionFolder : IExtensionFolder
291308 public static readonly TrivialExtensionFolder Instance = new ( ) ;
292309
293310 /// <summary>
294- /// No lock needed as registratin /unregistration must happen serially.
311+ /// No lock needed as registration /unregistration must happen serially.
295312 /// </summary>
296313 private readonly List < string > _registeredFilePaths = [ ] ;
297314
298- public ValueTask < AssemblyHandlers > RegisterAssemblyAsync ( string assemblyFilePath , CancellationToken cancellationToken )
315+ public AsyncLazy < AssemblyHandlers > RegisterAssembly ( string assemblyFilePath )
299316 {
300317 _registeredFilePaths . Add ( assemblyFilePath ) ;
301- return new ( AssemblyHandlers . Empty ) ;
318+ return AsyncLazy . Create ( AssemblyHandlers . Empty ) ;
302319 }
303320
304- public ValueTask < bool > UnregisterAssemblyAsync ( string assemblyFilePath , CancellationToken cancellationToken )
321+ public bool UnregisterAssembly ( string assemblyFilePath )
305322 {
306323 _registeredFilePaths . Remove ( assemblyFilePath ) ;
307- return new ( _registeredFilePaths . Count == 0 ) ;
324+ return _registeredFilePaths . Count == 0 ;
308325 }
309326
310327 public ValueTask AddHandlersAsync < TResult > ( string messageName , bool isSolution , ArrayBuilder < IExtensionMessageHandlerWrapper < TResult > > result , CancellationToken cancellationToken )
@@ -357,18 +374,15 @@ public static IExtensionFolder Create(
357374 }
358375 }
359376
360- public async ValueTask < AssemblyHandlers > RegisterAssemblyAsync (
361- string assemblyFilePath , CancellationToken cancellationToken )
377+ public AsyncLazy < AssemblyHandlers > RegisterAssembly ( string assemblyFilePath )
362378 {
363- var lazy = ImmutableInterlocked . GetOrAdd (
379+ return ImmutableInterlocked . GetOrAdd (
364380 ref _assemblyFilePathToHandlers ,
365381 assemblyFilePath ,
366382 static ( assemblyFilePath , @this ) => AsyncLazy . Create (
367383 static ( args , cancellationToken ) => CreateAssemblyHandlers ( args . @this , args . assemblyFilePath , cancellationToken ) ,
368384 ( assemblyFilePath , @this ) ) ,
369385 this ) ;
370-
371- return await lazy . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
372386 }
373387
374388 private static AssemblyHandlers CreateAssemblyHandlers (
@@ -422,10 +436,10 @@ public async ValueTask AddHandlersAsync<TResult>(string messageName, bool isSolu
422436 }
423437 }
424438
425- public ValueTask < bool > UnregisterAssemblyAsync ( string assemblyFilePath , CancellationToken cancellationToken )
439+ public bool UnregisterAssembly ( string assemblyFilePath )
426440 {
427- _assemblyFilePathToHandlers = _assemblyFilePathToHandlers . Remove ( assemblyFilePath ) ;
428- return new ( _assemblyFilePathToHandlers . IsEmpty ) ;
441+ _assemblyFilePathToHandlers . Remove ( assemblyFilePath ) ;
442+ return _assemblyFilePathToHandlers . IsEmpty ;
429443 }
430444 }
431445
0 commit comments