Skip to content
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

Merged
merged 1 commit into from
Feb 3, 2017

Conversation

dpoeschl
Copy link
Contributor

@dpoeschl dpoeschl commented Feb 2, 2017

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.

@Pilchie Pilchie added this to the 2.0 (RTM) milestone Feb 2, 2017
@dpoeschl
Copy link
Contributor Author

dpoeschl commented Feb 2, 2017

Tagging for review @dotnet/roslyn-ide

Pilchie
Pilchie previously approved these changes Feb 2, 2017
@@ -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;
Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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
Copy link
Member

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'?

Copy link
Contributor Author

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)
Copy link
Member

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?

Copy link
Contributor Author

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...

Copy link
Member

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;
Copy link
Member

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
Copy link
Member

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)
Copy link
Member

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.

@dpoeschl dpoeschl changed the title Avoid deadlocks when unloading .NET Standard Projects (directly or via Solution close) referencing Shared Projects Avoid deadlocks when unloading .NET Standard Projects (directly or via Solution close) referencing Shared Projects, or when unloading multitargeted Asp.NET projects Feb 2, 2017
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.

We now have a deadlock, unless we decide it's unimportant.

/// </summary>
private bool _isProjectUnloading;

private object _isProjectUnloadingLock = new object();
Copy link
Member

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)
Copy link
Member

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.

@dpoeschl dpoeschl changed the title Avoid deadlocks when unloading .NET Standard Projects (directly or via Solution close) referencing Shared Projects, or when unloading multitargeted Asp.NET projects Avoid deadlocks when unloading unloading multitargeted Asp.NET projects, or .NET Standard (CPS) Projects (directly or via Solution close) referencing Shared Projects Feb 2, 2017
@dpoeschl dpoeschl dismissed stale reviews from jasonmalinowski and Pilchie February 2, 2017 19:15

Code changes since last review.

@dpoeschl dpoeschl added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Feb 2, 2017
@dpoeschl
Copy link
Contributor Author

dpoeschl commented Feb 2, 2017

Hold off on reviewing for a minute. I forgot how to ThreadLocal but now I remember.

jasonmalinowski
jasonmalinowski previously approved these changes Feb 2, 2017
@@ -58,6 +78,7 @@ protected Workspace(HostServices host, string workspaceKind)
{
_primaryBranchId = BranchId.GetNextId();
_workspaceKind = workspaceKind;
_isProjectUnloading.Value = false;
Copy link
Member

Choose a reason for hiding this comment

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

Why?

@jasonmalinowski jasonmalinowski dismissed their stale review February 2, 2017 19:23

Oops you're right this is broken.

@dpoeschl dpoeschl removed the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Feb 2, 2017
@dpoeschl
Copy link
Contributor Author

dpoeschl commented Feb 2, 2017

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
Copy link
Member

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
Copy link
Member

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).
@dpoeschl
Copy link
Contributor Author

dpoeschl commented Feb 2, 2017

@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);
Copy link
Member

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?

Copy link
Contributor

@mattwar mattwar 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 seriously a bad idea.

@mattwar
Copy link
Contributor

mattwar commented Feb 2, 2017

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.

@Pilchie
Copy link
Member

Pilchie commented Feb 3, 2017

@mattwar can you and @dpoeschl discuss this tomorrow morning? This is a frequently reported hang, and we are running out of time to address it.

@dpoeschl
Copy link
Contributor Author

dpoeschl commented Feb 3, 2017

Talked to @mattwar about this this morning. We have ideas for how to do it better, but agreed it's acceptable to go with this for now. We'll leave #14479 open after this workaround to track the work of doing it in a less hacky way.

Copy link
Contributor

@mattwar mattwar left a 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.

@dpoeschl
Copy link
Contributor Author

dpoeschl commented Feb 3, 2017

Tagging @MattGertz for Escrow consideration.

@natidea
Copy link
Contributor

natidea commented Feb 3, 2017

Approved to merge via 296432

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Deadlock closing solutions with CPS projects referencing Shared Projects.
9 participants