-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Handle concurrent requests to update workspace contents and workspace SG info. #79628
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
Conversation
@@ -29,7 +29,7 @@ internal sealed class PdbMatchingSourceTextProvider() : IEventListener, IPdbMatc | |||
private readonly object _guard = new(); | |||
|
|||
private bool _isActive; | |||
private int _baselineSolutionVersion; | |||
private int _baselineSolutionContentVersion; |
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.
Renamed the generic 'SolutionVersion' concept in many placces to 'SolutionContentVersion' indicating that it revs as the content moves forward.
src/EditorFeatures/Core/EditAndContinue/PdbMatchingSourceTextProvider.cs
Outdated
Show resolved
Hide resolved
src/EditorFeatures/Core/EditAndContinue/PdbMatchingSourceTextProvider.cs
Show resolved
Hide resolved
@@ -53,7 +52,7 @@ internal sealed class LspWorkspaceManager : IDocumentChangeTracker, ILspService | |||
/// workspace). | |||
/// <para/> Access to this is guaranteed to be serial by the <see cref="RequestExecutionQueue{RequestContextType}"/> | |||
/// </summary> | |||
private readonly Dictionary<Workspace, (int? forkedFromVersion, Solution solution)> _cachedLspSolutions = []; | |||
private readonly Dictionary<Workspace, (int? forkedFromVersion, SourceGeneratorExecutionVersionMap? versionMap, Solution solution)> _cachedLspSolutions = []; |
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.
@dibarbet i wanted to maintain the semantics you had before as they may be important. in this case, we only attempt to remerge if the forked version and the execution version maps are the same.
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 think it is correct to only use the cached solution only if the version+execution version are the same.
Because if the workspace execution version changes, we would want to re-fork from the workspace to ensure we have the newly run generators (and not re-use the cached solution where generators did not run).
private static int DetermineNextSolutionStateContentVersion(Solution newSolution, Solution oldSolution) | ||
=> oldSolution.SolutionState == newSolution.SolutionState | ||
? oldSolution.SolutionStateContentVersion // If the solution state is the same, we can keep the same version. | ||
: oldSolution.SolutionStateContentVersion + 1; // Otherwise, increment the version. |
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.
A core part of the change. Content version only moves forward if hte content actually changes.
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.
To my other comment -- as far as I can tell this should be the only part that's necessary, and the other part isn't necessary at all...
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.
you are right :)
return true; | ||
} | ||
|
||
void ApplySourceGeneratorExecutionMap() |
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.
Major part of the change. Docs below describe the core logic we care about here.
@jasonmalinowski ptal. |
src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratorExecutionVersion.cs
Outdated
Show resolved
Hide resolved
…rExecutionVersion.cs Co-authored-by: Joey Robichaud <[email protected]>
@@ -53,7 +52,7 @@ internal sealed class LspWorkspaceManager : IDocumentChangeTracker, ILspService | |||
/// workspace). | |||
/// <para/> Access to this is guaranteed to be serial by the <see cref="RequestExecutionQueue{RequestContextType}"/> | |||
/// </summary> | |||
private readonly Dictionary<Workspace, (int? forkedFromVersion, Solution solution)> _cachedLspSolutions = []; | |||
private readonly Dictionary<Workspace, (int? forkedFromVersion, SourceGeneratorExecutionVersionMap? versionMap, Solution solution)> _cachedLspSolutions = []; |
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 think it is correct to only use the cached solution only if the version+execution version are the same.
Because if the workspace execution version changes, we would want to re-fork from the workspace to ensure we have the newly run generators (and not re-use the cached solution where generators did not run).
@@ -16,5 +16,5 @@ public static ImmutableArray<DocumentId> GetDocumentIds(this Solution solution, | |||
=> LanguageServer.Extensions.GetDocumentIds(solution, new(documentUri)); | |||
|
|||
public static int GetWorkspaceVersion(this Solution solution) | |||
=> solution.WorkspaceVersion; | |||
=> solution.SolutionStateContentVersion; |
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.
cc @davidwengier - I am not sure what you use this for, but you may want to check that this is correct (whether or not you care that this changes if the source generators execute)
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.
Thanks for tagging me. This is just a tie-break for two Razor documents, when their content matches, so anything that moves forward over time is good.
return (workspaceCurrentSolution, IsForked: false); | ||
} | ||
|
||
// Step 4: See if we can reuse a previously forked solution. | ||
if (cachedSolution != default && cachedSolution.forkedFromVersion == workspaceCurrentSolution.WorkspaceVersion) | ||
if (cachedSolution != default && |
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 add a test to LspWorkspaceManagerTests to verify this behavior?
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.
will do.
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.
done.
WFM. |
@jasonmalinowski ptal. |
|
@@ -1568,6 +1568,78 @@ internal async Task TestNonCompilationLanguage(SourceGeneratorExecutionPreferenc | |||
Assert.NotEqual(initialExecutionMap[noCompilationProject.Id], finalExecutionMap[noCompilationProject.Id]); | |||
} | |||
|
|||
[Theory, CombinatorialData] |
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'm not sure why this test is here versus SolutionWithSourceGeneratorTests.cs.
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.
c
private static int DetermineNextSolutionStateContentVersion(Solution newSolution, Solution oldSolution) | ||
=> oldSolution.SolutionState == newSolution.SolutionState | ||
? oldSolution.SolutionStateContentVersion // If the solution state is the same, we can keep the same version. | ||
: oldSolution.SolutionStateContentVersion + 1; // Otherwise, increment the version. |
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.
you are right :)
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.
Approved; question about why the test is in ServiceHubServicesTests still stands but requires no re-review.
It was just convenient. But i moved to SolutionWithSourceGenTests as that does make more sense :) |
Fixes #79587
Found by Jason in our integration tests (which often kick off an SG regen due to intellisense-builds completing) and then quickly trying to validate some other feature which can then collide with teh workspace changed caused by that.
The core issue is that tehre are two effective streams of 'changes' mutating the workspace. The first is some actual user-facing feature in question that attempts to call TryApplyChanges on the workspace to update it with whatever work it is doing. The second is that the workspace's host can call into it to tell it whne a file-save/project-build completes so that SG info for that file/project can be appropriately recomputed.
These operations are fundamentally racey in nature, and a request by a feature to update the workspace could fail if the host-request came in between the time that it captured the original solution and then tried to apply the new solution.
In practice this is unlikely to happen as the user would have to somehow do soemthing like kick off a build in between a feature starting/ending. However, it does happen in integration tests, and it does represent an unpleasant combination of factors that is hard to reason about.
To address this, we now separate out the concepts of a solution-content-change (literally something that changes something about the bitwise representation of the contents of the solution), versus the ambient SG execution versions (which control if we think we should rerun generators or not, regardless of the state of files). We still have the rule that if another content change snuck in that we will fail a TryApplyChanges. But we do allow the ambient SG execution versions to change.