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

Deadlock when enumerating resources with ResourceManager #74868

Closed
madelson opened this issue Aug 31, 2022 · 5 comments · Fixed by #75054
Closed

Deadlock when enumerating resources with ResourceManager #74868

madelson opened this issue Aug 31, 2022 · 5 comments · Fixed by #75054
Assignees
Milestone

Comments

@madelson
Copy link
Contributor

madelson commented Aug 31, 2022

Description

Enumerating resources with ResourceManager.GetResourceSet while concurrently calling ResourceManager.GetString can result in a hang which appears to be due to deadlock.

Reproduction Steps

The following code snippet hangs with high probability on .NET 6. Most commonly, this happens after printing the first entry on each thread.

void Main()
{
	var resourceType = typeof(MyResource); // standard resx with 15 entries
	var resourceManager = new ResourceManager(resourceType.FullName, resourceType.Assembly);
	
	var threads = 10;
	using var barrier = new Barrier(threads);
	Task.WaitAll(
		Enumerable.Range(0, threads)
			.Select(_ => Task.Run(() =>
			{
				barrier.SignalAndWait();
				EnumerateResources();
			}))
			.ToArray()
	); // never returns
	
	void EnumerateResources()
	{
		var set = resourceManager.GetResourceSet(CultureInfo.InvariantCulture, createIfNotExists: true, tryParents: true);
		foreach (var entry in set.Cast<DictionaryEntry>())
		{
			Console.WriteLine(resourceManager.GetString((string)entry.Key));
			Thread.Sleep(1);
		}
	}
}

Expected behavior

ResourceManager is supposed to be thread safe, so this code should work.

Actual behavior

Frequent hangs.

Regression?

I know that this used to be broken in some early versions of .NET Framework as well and was fixed at some point (perhaps in one of the 4.6 releases?). Testing the same snippet on .NET Framework latest does not hang, so I believe this is a regression.

EDIT: based on further testing this appears to be a regression vs. .NET Core 3.1 as well.

Known Workarounds

Allocate a new ResourceManager instance when planning to call GetResourceSet rather than re-using a shared instance.

Another sort-of workaround is to use the Key property on the enumerator instead of the Current property. This avoids the deadlock and makes more progress, but we instead run into #74052 .

Configuration

.NET 6 (6.0.5) Windows 10 x64.

Other information

In RuntimeResourceSet, we lock the cache first and then lock the ResourceReader.

In ResourceReader.ResourceEnumerator, we lock the reader first and then the cache.

I believe that this is the cause of the deadlock.

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Aug 31, 2022
@ghost
Copy link

ghost commented Aug 31, 2022

Tagging subscribers to this area: @dotnet/area-system-resources
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

Enumerating resources with ResourceManager.GetResourceSet while concurrently calling ResourceManager.GetString can result in a hang which appears to be due to deadlock.

Reproduction Steps

The following code snippet hangs with high probability on .NET 6. Most commonly, this happens after printing the first entry on each thread.

void Main()
{
	var resourceType = typeof(MyResource); // standard resx with 15 entries
	var resourceManager = new ResourceManager(resourceType.FullName, resourceType.Assembly);
	
	var threads = 10;
	using var barrier = new Barrier(threads);
	Task.WaitAll(
		Enumerable.Range(0, threads)
			.Select(_ => Task.Run(() =>
			{
				barrier.SignalAndWait();
				EnumerateResources();
			}))
			.ToArray()
	); // never returns
	
	void EnumerateResources()
	{
		var set = resourceManager.GetResourceSet(CultureInfo.InvariantCulture, createIfNotExists: true, tryParents: true);
		foreach (var entry in set.Cast<DictionaryEntry>())
		{
			Console.WriteLine(resourceManager.GetString((string)entry.Key));
			Thread.Sleep(1);
		}
	}
}

Expected behavior

ResourceManager is supposed to be thread safe, so this code should work.

Actual behavior

Frequent hangs.

Regression?

I know that this used to be broken in some early versions of .NET Framework as well and was fixed at some point (perhaps in one of the 4.6 releases?). Testing the same snippet on .NET Framework latest does not hang, so I believe this is a regression.

Known Workarounds

Allocate a new ResourceManager instance when planning to call GetResourceSet rather than re-using a shared instance.

Another sort-of workaround is to use the Key property on the enumerator instead of the Current property. This avoids the deadlock and makes more progress, but we instead run into #74052 .

Configuration

.NET 6 (6.0.5) Windows 10 x64.

Other information

In RuntimeResourceSet, we lock the cache first and then lock the ResourceReader.

In ResourceReader.ResourceEnumerator, we lock the reader first and then the cache.

I believe that this is the cause of the deadlock.

Author: madelson
Assignees: -
Labels:

area-System.Resources

Milestone: -

@madelson
Copy link
Contributor Author

I'd be happy to work on this as part of working on #74052

madelson added a commit to madelson/runtime that referenced this issue Sep 3, 2022
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
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Sep 3, 2022
@joperezr
Copy link
Member

joperezr commented Sep 6, 2022

Thanks for the report @madelson, and also for checking when did this regressed. From your original post though, I see that this issue was present in .NET 6 (meaning it wasn't a regression from .NET 6 to .NET 7), so I'm going to move this issue out of .NET 7 by setting the milestone to .NET 8 for now. Thanks again for working on this!

@joperezr joperezr added this to the 8.0.0 milestone Sep 6, 2022
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Sep 6, 2022
@madelson
Copy link
Contributor Author

madelson commented Sep 6, 2022

@joperezr it isn't a regression from 6->7, but it is a regression from the previous LTS release (3.1) to the current LTS release (6). I imagine that the bar is too high to consider backporting this to 6 but given the severity of the issue if you encounter it (deadlocking the app) and the fact that apps may be unprepared for this given its previous fix, it would be nice to at least consider that.

@joperezr
Copy link
Member

joperezr commented Sep 6, 2022

That's right, I didn't mean that this shouldn't get fixed in servicing, sorry for not making that clear. What I meant was that the fix is likely not going to hit .NET 7 before we ship, so I just adjusted the milestone to vNext for now. The process to get this regression fixed in servicing would be to:

  1. Fix the issue in main branch (which today means 8.0 and the reason why I picked that milestone)
  2. Once the fix is ready and we have validated that the fix works as expected, we can then make a case for us to service this in all impacted releases (which would be 6.0, and 7.0). If approved, then we can cherry-pick the change from main into those servicing branches and create PRs for them in order to get the fix serviced.

stephentoub pushed a commit that referenced this issue Jan 3, 2023
* Fix thread-safety issues with enumerating ResourceManager.

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

* Remove trailing whitespace

* Address feedback from #75054

* Add comment in response to #75054 (comment)

* Implement feedback from #75054
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jan 3, 2023
github-actions bot pushed a commit that referenced this issue Jan 27, 2023
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
github-actions bot pushed a commit that referenced this issue Jan 27, 2023
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
@ghost ghost locked as resolved and limited conversation to collaborators Feb 2, 2023
carlossanlop pushed a commit that referenced this issue Feb 10, 2023
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
carlossanlop pushed a commit that referenced this issue Mar 10, 2023
…er. (#81283)

* Fix thread-safety issues with enumerating ResourceManager.

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

* Remove trailing whitespace

* Address feedback from #75054

* Add comment in response to #75054 (comment)

* Implement feedback from #75054

* Increase timeout for TestResourceManagerIsSafeForConcurrentAccessAndEnumeration (#80330)

This raises the timeout to 30s, the same as what we have for the equivalent ResourceManager test (https://github.com/dotnet/runtime/blob/15fcb990fe17348ab6ddde0939200b900169920b/src/libraries/System.Resources.ResourceManager/tests/ResourceManagerTests.cs#L255).

fix #80277

---------

Co-authored-by: Michael Adelson <mike.adelson314@gmail.com>
Co-authored-by: madelson <1269046+madelson@users.noreply.github.com>
Co-authored-by: Buyaa Namnan <bunamnan@microsoft.com>
carlossanlop added a commit that referenced this issue Mar 10, 2023
…er. (#81281)

* Fix thread-safety issues with enumerating ResourceManager.

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

* Remove trailing whitespace

* Address feedback from #75054

* Add comment in response to #75054 (comment)

* Implement feedback from #75054

* Increase timeout for TestResourceManagerIsSafeForConcurrentAccessAndEnumeration (#80330)

This raises the timeout to 30s, the same as what we have for the equivalent ResourceManager test (https://github.com/dotnet/runtime/blob/15fcb990fe17348ab6ddde0939200b900169920b/src/libraries/System.Resources.ResourceManager/tests/ResourceManagerTests.cs#L255).

fix #80277

---------

Co-authored-by: Michael Adelson <mike.adelson314@gmail.com>
Co-authored-by: madelson <1269046+madelson@users.noreply.github.com>
Co-authored-by: Carlos Sanchez <1175054+carlossanlop@users.noreply.github.com>
Co-authored-by: Buyaa Namnan <bunamnan@microsoft.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants