-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Avoid deadlocks when unloading unloading multitargeted Asp.NET projects, or .NET Standard (CPS) Projects (directly or via Solution close) referencing Shared Projects #16889
Conversation
Tagging for review @dotnet/roslyn-ide |
@@ -424,6 +426,8 @@ protected internal virtual void OnProjectRemoved(ProjectId projectId) | |||
var newSolution = this.SetCurrentSolution(oldSolution.RemoveProject(projectId)); | |||
|
|||
this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectRemoved, oldSolution, newSolution, projectId); | |||
|
|||
_isProjectUnloading = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is anything ever expected to throw between these two points? Should this be in a finally
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on the finally. If only because someone might put a return in this code in the future and completely f-up the system.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -22,6 +22,26 @@ public abstract partial class Workspace | |||
private readonly Dictionary<DocumentId, TextTracker> _textTrackers = new Dictionary<DocumentId, TextTracker>(); | |||
|
|||
/// <summary> | |||
/// Unloading projects takes a serialization lock, which can cause Shared Project | |||
/// IVsHierarchys to notify us that their context hierarchy has changed. In response to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't understand the very first statement. how is taking a serialization lock causing 'Shared Project Hierarchys to notify us that their context hierarchy has changed'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"lock, which can cause" ==> "lock and can cause"
@@ -335,7 +355,14 @@ protected void OnDocumentContextUpdated(DocumentId documentId) | |||
|
|||
if (container != null) | |||
{ | |||
OnDocumentContextUpdated(documentId, container); | |||
if (_isProjectUnloading) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reading of mutable state outside of the lock. How do we know this is safe? Do we have threading asserts about when these things will happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do not have threading asserts, but they both happen on the UI thread in practice...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is considered free threaded, which is why there is no threading asserts. It should be made safe.
@@ -424,6 +426,8 @@ protected internal virtual void OnProjectRemoved(ProjectId projectId) | |||
var newSolution = this.SetCurrentSolution(oldSolution.RemoveProject(projectId)); | |||
|
|||
this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectRemoved, oldSolution, newSolution, projectId); | |||
|
|||
_isProjectUnloading = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -22,6 +22,26 @@ public abstract partial class Workspace | |||
private readonly Dictionary<DocumentId, TextTracker> _textTrackers = new Dictionary<DocumentId, TextTracker>(); | |||
|
|||
/// <summary> | |||
/// Unloading projects takes a serialization lock, which can cause Shared Project |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you name which function that lock is being taken in here? With a cref if possible? 😉
@@ -335,7 +355,14 @@ protected void OnDocumentContextUpdated(DocumentId documentId) | |||
|
|||
if (container != null) | |||
{ | |||
OnDocumentContextUpdated(documentId, container); | |||
if (_isProjectUnloading) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is considered free threaded, which is why there is no threading asserts. It should be made safe.
8d620be
to
9a8f5d9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We now have a deadlock, unless we decide it's unimportant.
/// </summary> | ||
private bool _isProjectUnloading; | ||
|
||
private object _isProjectUnloadingLock = new object(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a comment explaining the lock hierarchy here.
@@ -335,7 +335,17 @@ protected void OnDocumentContextUpdated(DocumentId documentId) | |||
|
|||
if (container != null) | |||
{ | |||
OnDocumentContextUpdated(documentId, container); | |||
lock (_isProjectUnloadingLock) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now a deadlock. If OnProjectRemoved takes the serialization lock but hadn't yet taken _isProjectUnloadingLock, and then this acquires this lock here, we will call OnDocumentContextUpdated and try to take the serialization lock. The first thread now can't acquire _isProjectUnloadingLock and we're toasted.
9a8f5d9
to
db4350c
Compare
Code changes since last review.
Hold off on reviewing for a minute. I forgot how to ThreadLocal but now I remember. |
@@ -58,6 +78,7 @@ protected Workspace(HostServices host, string workspaceKind) | |||
{ | |||
_primaryBranchId = BranchId.GetNextId(); | |||
_workspaceKind = workspaceKind; | |||
_isProjectUnloading.Value = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
Oops you're right this is broken.
db4350c
to
6323e91
Compare
Okay, good to review again. @jasonmalinowski @CyrusNajmabadi @Pilchie @dotnet/roslyn-ide |
@@ -50,6 +50,26 @@ public abstract partial class Workspace : IDisposable | |||
private Action<string> _testMessageLogger; | |||
|
|||
/// <summary> | |||
/// Unloading projects takes a serialization lock, but can also cause Shared Project |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you change this to a cref to the OnProjectRemoved since that's the operation here from the perspective of this type?
@@ -50,6 +50,26 @@ public abstract partial class Workspace : IDisposable | |||
private Action<string> _testMessageLogger; | |||
|
|||
/// <summary> | |||
/// Unloading projects takes a serialization lock, but can also cause Shared Project | |||
/// IVsHierarchys to notify us that their context hierarchy has changed. In response to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"...that their context hierarchy has changed while we are holding the lock." since that's the CS101 rule we're violating here.
…a Solution close) referencing Shared Projects Fixes dotnet#14479 The problem here is that unloading projects takes a serialization lock, which can cause Shared Project IVsHierarchys to notify us that their context hierarchy has changed. In response to this, we try to set the new active context document for open files, which also tries to take the serialization lock, and we deadlock. For .NET Framework Projects that reference Shared Projects, two things prevent deadlocks when projects unload. During solution close, any Shared Projects are disconnected before the projects start to unload, so no IVsHierarchy events are fired. During a single project unload, we receive notification of the context hierarchy change before the project is unloaded, avoiding any IVsHierarchy events if we tell the shared hierarchy to set its context hierarchy to what it already is. Neither of these behaviors are safe to rely on with .NET Standard projects, so we have to prevent the deadlock ourselves. We do this by remembering if we're already in the serialization lock due to project unload, and then not take the lock to update document contexts if so (but continuing to lock if it's not during a project unload).
6323e91
to
e2cc9b6
Compare
@Pilchie @CyrusNajmabadi I have two signoffs, but you both showed interest in this so I want to give you a chance to opine before moving forward. |
/// the serialization lock due to project unload, and then not take the lock to update | ||
/// document contexts if so (but continuing to lock if it's not during a project unload). | ||
/// </summary> | ||
private ThreadLocal<bool> _isProjectUnloading = new ThreadLocal<bool>(() => false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's still not obvious to me why this needs to be a ThreadLocal
. Can you clarify?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is seriously a bad idea.
There shouldn't be any reentrancy at the workspace layer regardless of unloading or not. Why is there a deadlock? What code is causing the loop back to the lock such that you need to hack it to let it execute? That code is the problem. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On condition that we do the right fix after RTM.
Tagging @MattGertz for Escrow consideration. |
Approved to merge via 296432 |
Fixes #14479
The problem here is that unloading projects takes a serialization lock,
which can cause Shared Project IVsHierarchys to notify us that their
context hierarchy has changed while the lock is still held. In response
to this, we try to set the new active context document for open files,
which also tries to take the serialization lock, and we deadlock.
For .NET Framework Projects that reference Shared Projects, two things
prevent deadlocks when projects unload. During solution close, any
Shared Projects are disconnected before the projects start to unload, so
no IVsHierarchy events are fired. During a single project unload, we
receive notification of the context hierarchy change before the project
is unloaded, avoiding any IVsHierarchy events if we tell the shared
hierarchy to set its context hierarchy to what it already is.
Neither of these behaviors are safe to rely on with .NET Standard (CPS)
or multitargeted Asp.NET projects, so we have to prevent the deadlock
ourselves. We do this by remembering if we're already in the
serialization lock due to project unload, and then not take the lock to
update document contexts if so (but continuing to lock if it's not
during a project unload).
Escrow Template
Customer scenario In a multitargeted Asp.NET project, closing the solution with open documents can cause the product to hang. In regular CPS projects (e.g. .NET Standard) that reference a Shared Project, closing the solution with open documents belonging to the Shared Project causes the product to hang. In both cases, it can hang before saving the solution.
Bugs this fixes:
#14479
[Tracking] https://devdiv.visualstudio.com/DevDiv/_workitems?_a=edit&id=296432
Workarounds, if any: Close all open documents before closing the solution.
Risk: Fairly low. We avoid taking a lock when the lock has already been taken for us.
Performance impact: Essentially none. No allocations or complexity changes.
Is this a regression from a previous update?: No. It's been broken since CPS projects started being used.
Root cause analysis: The Roslyn Workspace was getting lucky that the old project system handled Shared Projects in a way that naturally avoided this reentrancy.
How did we miss it? Everything worked fine with the old project system, so it was never noticed.
How was the bug found? Automated tests and exploratory testing by the CPS team. I don't know of any customer reports.