-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Fix thread-safety issues with enumerating ResourceManager. #75054
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
Concurrently enumerating a ResourceManager while also calling GetString() and similar methods was prone to both transient errors and deadlock. The transient errors were caused by RuntimeResourceSet calling internal methods on ResourceReader that did not properly lock. Now, all exposed methods on the Reader are thread-safe. The deadlock was called by inconsistent lock ordering between ResourceReader.ResourceEnumerator and RuntimeResourceSet which both lock on the RuntimeResourceSet's cache and on the ResourceReader itself. Now, the enumerator does not need to take both locks at the same time. Fix dotnet#74052 Fix dotnet#74868
|
Tagging subscribers to this area: @dotnet/area-system-resources Issue DetailsConcurrently enumerating a ResourceManager while also calling GetString() The transient errors were caused by RuntimeResourceSet calling internal The deadlock was called by inconsistent lock ordering between
|
| "the user to only get one error.")] | ||
| private object DeserializeObject(int typeIndex) | ||
| { | ||
| Debug.Assert(Monitor.IsEntered(this)); |
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 added these to private methods that access _store per this suggestion.
src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs
Show resolved
Hide resolved
|
|
||
| // Normally calling LoadString or LoadObject requires | ||
| // taking a lock. Note that in this case, we took a | ||
| // lock before calling this method. |
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 comment was not (no longer?) correct. We do typically call these within a lock, but it is a lock on either _resCache or _caseInsensitiveTable. That lock was previously making things thread-safe so long as the caller was only using GetString/GetObject and was not flipping ResourceManager.IgnoreCase on and off.
Now, LoadString/LoadObject do lock internally so there is no edge case here.
| using Barrier barrier = new(Threads); | ||
| Task task = Task.WhenAll(Enumerable.Range(0, Threads).Select(_ => Task.Run(WaitForBarrierThenEnumerateResources))); | ||
|
|
||
| Assert.True(task.Wait(TimeSpan.FromSeconds(10))); |
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.
Before my fixes, this test reliably would reproduce both issues on my machine. Let me know if there is a better way to test for concurrency issues like this.
I do think it is very important to have such a test, because this problem was actually previously fixed in 2015 and then broken again in 2020 (I think the break happened here).
| <Content Include="Resources\icon.ico" | ||
| Link="%(Filename)%(Extension)" | ||
| CopyToOutputDirectory="PreserveNewest" | ||
| Visible="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.
Reformatting was done by VS
| <EmbeddedResource Include="Resources\AToZResx.resx"> | ||
| <Generator>ResXFileCodeGenerator</Generator> | ||
| <LastGenOutput>AToZResx.Designer.cs</LastGenOutput> | ||
| </EmbeddedResource> |
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 what VS generated when I made a new resx file. Any reason to change it to match what the other files were using?
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'd be nice to be consistent.
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.
nvm reading this again I think it is consistent with the others
src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs
Show resolved
Hide resolved
| "the user to only get one error.")] | ||
| private object DeserializeObject(int typeIndex) | ||
| { | ||
| Debug.Assert(Monitor.IsEntered(this)); |
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 we also add a comment explaining why this is being added?
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.
After double checking I don't think we need this one; this method doesn't access _store directly. Removed
| "Custom readers as well as custom objects on the resources file are not observable by the trimmer and so required assemblies, types and members may be removed.")] | ||
| private bool InitializeBinaryFormatter() | ||
| { | ||
| Debug.Assert(Monitor.IsEntered(this)); |
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.
Same here. That way in the future people that look at this code will know why is the protection expected.
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.
Removed this one too. Not needed.
| [Theory] | ||
| [InlineData(false)] | ||
| [InlineData(true)] | ||
| public static void TestResourceManagerIsSafeForConcurrentAccessAndEnumeration(bool useEnumeratorEntry) |
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.
Does it make sense to add equivalent tests in System.Resources.Extensions as well and perhaps with a resource file that has something other than strings?
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.
Sorry for the delay in reviewing this @madelson, I've left some comments but overall this is looking good, thanks for the contribution.
Thanks @joperezr . I've pushed a change addressing your feedback. Let me know if there is anything else to do. |
What about |
|
Same for |
|
So following the lines of my last two comments, there seems to be two different approaches when
There are some places where we still use |
@joperezr this one can't have an assertion because it is called both during initialization (which per my comment on Do you think it is worth adding a comment like this to SkipString()?:
This one accesses
I believe all of those places are called during initialization, which is called out by the comment on In cases which are always called during initialization (e.g. |
|
I see thanks for the explanation and sorry that I missed some of those comments.
Yes, I do think this would be good for consistency. |
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.
Apart from my last NIT of just adding a comment to SkipString this change looks good to me. @stephentoub since you commented on this PR earlier, any thoughts on the reply from @madelson?
|
|
||
| const int Threads = 10; | ||
| using Barrier barrier = new(Threads); | ||
| Task task = Task.WhenAll(Enumerable.Range(0, Threads).Select(_ => Task.Run(WaitForBarrierThenEnumerateResources))); |
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.
All of the same comments as above.
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.
Doen
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 few comments, but otherwise LGTM. Thanks.
|
@joperezr looking at the new build failures these seem unrelated? https://helixre107v0xdeko0k025g8.blob.core.windows.net/dotnet-runtime-refs-pull-75054-merge-b07193f9df6f4ac4a6/JIT.1/1/console.735a06a0.log?helixlogtype=result what do you think? |
|
The new failure is #79517 |
|
@joperezr anything else to do here? |
|
/backport to release/6.0 |
|
Started backporting to release/6.0: https://github.com/dotnet/runtime/actions/runs/4027066289 |
|
@buyaa-n assuming you also plan to backport to 7.0? |
|
/backport to release/7.0 |
|
Started backporting to release/7.0: https://github.com/dotnet/runtime/actions/runs/4027191479 |
Concurrently enumerating a ResourceManager while also calling GetString()
and similar methods was prone to both transient errors and deadlock.
The transient errors were caused by RuntimeResourceSet calling internal
methods on ResourceReader that did not properly lock. Now, all exposed
methods on the Reader are thread-safe.
The deadlock was called by inconsistent lock ordering between
ResourceReader.ResourceEnumerator and RuntimeResourceSet which both lock
on the RuntimeResourceSet's cache and on the ResourceReader itself. Now,
the enumerator does not need to take both locks at the same time.
Fix #74052
Fix #74868