Skip to content

Commit 16a0b29

Browse files
CHange how locking works
1 parent a2412f3 commit 16a0b29

File tree

4 files changed

+70
-72
lines changed

4 files changed

+70
-72
lines changed

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

Lines changed: 68 additions & 54 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<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

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,12 @@ internal interface IExtensionMessageHandlerService : IWorkspaceService
1818
/// </summary>
1919
/// <param name="assemblyFilePath">The assembly to register and create message handlers from.</param>
2020
/// <returns>The names of the registered handlers.</returns>
21-
/// <remarks>Should be called serially with other <see cref="RegisterExtensionAsync"/>, <see
22-
/// cref="UnregisterExtensionAsync"/>, or <see cref="ResetAsync"/> calls.</remarks>
2321
ValueTask<RegisterExtensionResponse> RegisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken);
2422

2523
/// <summary>
2624
/// Unregisters extension message handlers previously registered from <paramref name="assemblyFilePath"/>.
2725
/// </summary>
2826
/// <param name="assemblyFilePath">The assembly for which handlers should be unregistered.</param>
29-
/// <remarks>Should be called serially with other <see cref="RegisterExtensionAsync"/>, <see
30-
/// cref="UnregisterExtensionAsync"/>, or <see cref="ResetAsync"/> calls.</remarks>
3127
ValueTask UnregisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken);
3228

3329
/// <summary>
@@ -44,7 +40,6 @@ internal interface IExtensionMessageHandlerService : IWorkspaceService
4440
/// <param name="messageName">The name of the handler to execute. This is generally the full name of the type implementing the handler.</param>
4541
/// <param name="jsonMessage">The json message to be passed to the handler.</param>
4642
/// <returns>The json message returned by the handler.</returns>
47-
/// <remarks>Can be called concurrently with other message requests.</remarks>
4843
ValueTask<string> HandleExtensionWorkspaceMessageAsync(
4944
Solution solution,
5045
string messageName,
@@ -58,7 +53,6 @@ ValueTask<string> HandleExtensionWorkspaceMessageAsync(
5853
/// <param name="messageName">The name of the handler to execute. This is generally the full name of the type implementing the handler.</param>
5954
/// <param name="jsonMessage">The json message to be passed to the handler.</param>
6055
/// <returns>The json message returned by the handler.</returns>
61-
/// <remarks>Can be called concurrently with other message requests.</remarks>
6256
ValueTask<string> HandleExtensionDocumentMessageAsync(
6357
Document documentId,
6458
string messageName,

src/LanguageServer/Protocol/Handler/Extensions/ExtensionRegisterHandler.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,7 @@ internal sealed class ExtensionRegisterHandler()
2020
{
2121
private const string MethodName = "roslyn/extensionRegister";
2222

23-
/// <summary>
24-
/// Report that we mutate solution state so that we only attempt to register or unregister one extension at a time.
25-
/// This ensures we don't have to handle any threading concerns while this is happening. As this should be a rare
26-
/// operation, this simplifies things while ideally being low cost.
27-
/// </summary>
28-
public bool MutatesSolutionState => true;
23+
public bool MutatesSolutionState => false;
2924

3025
public bool RequiresLSPSolution => true;
3126

src/LanguageServer/Protocol/Handler/Extensions/ExtensionUnregisterHandler.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,7 @@ internal sealed class ExtensionUnregisterHandler()
2020
{
2121
private const string MethodName = "roslyn/extensionUnregister";
2222

23-
/// <summary>
24-
/// Report that we mutate solution state so that we only attempt to register or unregister one extension at a time.
25-
/// This ensures we don't have to handle any threading concerns while this is happening. As this should be a rare
26-
/// operation, this simplifies things while ideally being low cost.
27-
/// </summary>
28-
public bool MutatesSolutionState => true;
23+
public bool MutatesSolutionState => false;
2924

3025
public bool RequiresLSPSolution => true;
3126

0 commit comments

Comments
 (0)