Bug #110717 Opening two MySqlConnections simultaneously can crash
Submitted: 18 Apr 2023 15:17 Modified: 5 Sep 2023 18:32
Reporter: Bradley Grainger (OCA) Email Updates:
Status: Closed Impact on me:
None 
Category:Connector / NET Severity:S2 (Serious)
Version:8.0.33 OS:Windows (10)
Assigned to: CPU Architecture:Any

[18 Apr 2023 15:17] Bradley Grainger
Description:
If multiple MySqlConnection objects are opened at the same time (on different threads), the application may crash with an ArgumentException or InvalidOperationException when MySqlPoolManager.GetPoolAsync tries to modify the shared 'Pools' object concurrently from different threads.

Example exceptions:

❯ dotnet run
Unhandled exception. System.AggregateException: One or more errors occurred. (An item with the same key has already been added. Key: server=localhost;user id=root;password=pass) (Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.) (Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.) (Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.)

 ---> System.ArgumentException: An item with the same key has already been added. Key: server=localhost;user id=root;password=pass
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at MySql.Data.MySqlClient.MySqlPoolManager.GetPoolAsync(MySqlConnectionStringBuilder settings, Boolean execAsync, CancellationToken cancellationToken)
   at MySql.Data.MySqlClient.MySqlConnection.OpenAsync(Boolean execAsync, CancellationToken cancellationToken)
   at MySql.Data.MySqlClient.MySqlConnection.Open()
   at z.Program.<>c__DisplayClass1_0.<Main>b__0() in C:\Test\Program.cs:line 43
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.WaitAllCore(Task[] tasks, Int32 millisecondsTimeout, CancellationToken cancellationToken)
   at System.Threading.Tasks.Task.WaitAll(Task[] tasks)
   at z.Program.Main(String[] args) in C:\Test\Program.cs:line 47

 ---> (Inner Exception #1) System.InvalidOperationException: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at MySql.Data.MySqlClient.MySqlPoolManager.GetPoolAsync(MySqlConnectionStringBuilder settings, Boolean execAsync, CancellationToken cancellationToken)
   at MySql.Data.MySqlClient.MySqlConnection.OpenAsync(Boolean execAsync, CancellationToken cancellationToken)
   at MySql.Data.MySqlClient.MySqlConnection.Open()
   at z.Program.<>c__DisplayClass1_0.<Main>b__0() in C:\Test\Program.cs:line 43
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)<---

 ---> (Inner Exception #2) System.InvalidOperationException: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at MySql.Data.MySqlClient.MySqlPoolManager.GetPoolAsync(MySqlConnectionStringBuilder settings, Boolean execAsync, CancellationToken cancellationToken)
   at MySql.Data.MySqlClient.MySqlConnection.OpenAsync(Boolean execAsync, CancellationToken cancellationToken)
   at MySql.Data.MySqlClient.MySqlConnection.Open()
   at z.Program.<>c__DisplayClass1_0.<Main>b__0() in C:\Test\Program.cs:line 43
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)<---

 ---> (Inner Exception #3) System.InvalidOperationException: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at MySql.Data.MySqlClient.MySqlPoolManager.GetPoolAsync(MySqlConnectionStringBuilder settings, Boolean execAsync, CancellationToken cancellationToken)
   at MySql.Data.MySqlClient.MySqlConnection.OpenAsync(Boolean execAsync, CancellationToken cancellationToken)
   at MySql.Data.MySqlClient.MySqlConnection.Open()
   at z.Program.<>c__DisplayClass1_0.<Main>b__0() in C:\Test\Program.cs:line 43
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)<---

How to repeat:
Run the following C# program. (If necessary, increase the number of iterations to trigger the bug.)

var connectionString = "server=localhost;user id=root;password=pass";
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
	tasks.Add(Task.Run(() =>
	{
		using (var connection = new MySqlConnection(connectionString))
		{
			connection.Open();
		}
	}));
}
Task.WaitAll(tasks.ToArray());

Suggested fix:
Change GetPoolAsync to use a shared static SemaphoreSlim object (and wait on that), instead of creating a local variable.
[19 Apr 2023 8:07] MySQL Verification Team
Hello Bradley Grainger,

Thank you for the bug report.
Verified as described.

regards,
Umesh
[18 Jul 2023 13:12] Stuart Lang
I've pushed a PR, I don't necessarily think it's the right solution, but should highlight the various areas that need to be looked at:
https://github.com/mysql/mysql-connector-net/pull/56
[18 Jul 2023 13:29] Stuart Lang
I didn't mean to reply to this bug 🤦‍♂️ but it is still relevant here
[10 Aug 2023 4:56] MySQL Verification Team
Bug #111990 marked as duplicate of this one
[5 Sep 2023 18:32] Christine Cole
Posted by developer:
 
Fixed as of the upcoming MySQL Connector/NET 8.2.0 release, and here's the proposed changelog entry from the documentation team:

Multiple MySqlConnection objects on separate threads called simultaneously
could return ArgumentException or InvalidOperationException when
MySqlPoolManager.GetPoolAsync tried to modify the shared thread-pool
object concurrently from the different threads.

Thank you for the bug report.
[13 Sep 2023 13:01] MySQL Verification Team
Bug #112321 marked as duplicate of this one.