You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Npgsql 7.0.2 blocks threads NpgsqlConnector.PerformUserCancellation causing connection pool exhaustion #5032
I have not been able to create a sample to reproduce yet, and I'm not able to share our source code. I will keep trying and share if I can reproduce.
The issue
After upgrading Npgsql NuGet package rom 7.0.1 to 7.0.2 we are seeing idle connections not getting closed after several days. The number of open connections gradually increased until hitting our max pool size (1000), after which every query failed since the connection pool was exhausted.
Our process thread count also increased until it plateued around 1000 when the max pool size was reached.
Reverting the Npgsql NuGet package back to 7.0.1 fixed the issue.
Stack trace:
System.Private.CoreLib!System.Threading.ManualResetEventSlim.Wait(int32,value class System.Threading.CancellationToken)
System.Private.CoreLib!System.Threading.ManualResetEventSlim.Wait()
Npgsql!Npgsql.Internal.NpgsqlConnector.PerformUserCancellation()
Npgsql!Npgsql.Internal.NpgsqlConnector+<>c.<StartNestedCancellableOperation>b__262_0(class System.Object)
System.Private.CoreLib!System.Threading.CancellationTokenSource.Register(class System.Delegate,class System.Object,class System.Threading.SynchronizationContext,class System.Threading.ExecutionContext)
Npgsql!Npgsql.NpgsqlDataReader+<NextResult>d__47.MoveNext()
System.Private.CoreLib!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
Npgsql!Npgsql.NpgsqlDataReader.NextResultAsync(value class System.Threading.CancellationToken)
Npgsql!Npgsql.NpgsqlCommand+<ExecuteReader>d__124.MoveNext()
System.Private.CoreLib!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
Npgsql!Npgsql.NpgsqlCommand.ExecuteReader(value class System.Data.CommandBehavior,bool,value class System.Threading.CancellationToken)
Npgsql!Npgsql.NpgsqlCommand.ExecuteReaderAsync(value class System.Data.CommandBehavior,value class System.Threading.CancellationToken)
Npgsql!Npgsql.NpgsqlCommand+<ExecuteDbDataReaderAsync>d__117.MoveNext()
System.Private.CoreLib!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
Npgsql!Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(value class System.Data.CommandBehavior,value class System.Threading.CancellationToken)
I noticed there is a new ManualResetEventSlim added and used in the PerformUserCancellation method that was not there in 7.0.1 as well as some other changes in this method.
Yeah, had to add ManualResetEventSlim there to fix #4906. Just in case, are there any other stuck threads? I'm going to take a look in the next few days but any info will be helpful.
Yeah, OK. The logic there is, that we first reset mres, then register a callback on CancellationToken, and then read from the socket (and setting mres at some point). Now, registering a callback on CancellationToken can immediately trigger that callback synchronously in case if CancellationToken is already cancelled. So I guess that's what happens. The thread which is supposed to read from the socket actually stuck on mres.
If it helps, there were a few threads that had a slightly different stack.
In the dotnet-stack output we captured from a process with 446 blocked threads, there were 16 with the stack I pasted above, and 430 with the shorter stack below, missing the StartNestedCancellableOperation line.
System.Private.CoreLib!System.Threading.ManualResetEventSlim.Wait(int32,value class System.Threading.CancellationToken)
Npgsql!Npgsql.Internal.NpgsqlConnector.PerformUserCancellation()
System.Private.CoreLib!System.Threading.CancellationTokenSource.Register(class System.Delegate,class System.Object,class System.Threading.SynchronizationContext,class System.Threading.ExecutionContext)
Npgsql!Npgsql.NpgsqlDataReader+<NextResult>d__47.MoveNext()
System.Private.CoreLib!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
Npgsql!Npgsql.NpgsqlDataReader.NextResultAsync(value class System.Threading.CancellationToken)
Npgsql!Npgsql.NpgsqlCommand+<ExecuteReader>d__124.MoveNext()
System.Private.CoreLib!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
Npgsql!Npgsql.NpgsqlCommand.ExecuteReader(value class System.Data.CommandBehavior,bool,value class System.Threading.CancellationToken)
Npgsql!Npgsql.NpgsqlCommand.ExecuteReaderAsync(value class System.Data.CommandBehavior,value class System.Threading.CancellationToken)
Npgsql!Npgsql.NpgsqlCommand+<ExecuteDbDataReaderAsync>d__117.MoveNext()
System.Private.CoreLib!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
Npgsql!Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(value class System.Data.CommandBehavior,value class System.Threading.CancellationToken)
The stack might be different due to jit recompiling that method from tier 0 (fast compilation, minimum of optimizations) to tier 1 (longer compilation, more optimization options, like method inlining). But looking at the stack trace it does indeed confirm that I was right. If the callback isn't called synchronously, we would have gotten a stack trace like this:
atConsoleApp30.Program.<>c.<MyMethod>b__1_0()
at System.Threading.CancellationToken.<>c.<Register>b__12_0(Objectobj)
at System.Threading.CancellationTokenSource.Invoke(Delegated,Objectstate,CancellationTokenSourcesource)
at System.Threading.CancellationTokenSource.CallbackNode.<>c.<ExecuteCallback>b__9_0(Objects)
at System.Threading.ExecutionContext.RunInternal(ExecutionContextexecutionContext,ContextCallbackcallback,Objectstate)
Activity
vonzshik commentedon Apr 12, 2023
Yeah, had to add
ManualResetEventSlimthere to fix #4906. Just in case, are there any other stuck threads? I'm going to take a look in the next few days but any info will be helpful.vonzshik commentedon Apr 12, 2023
Looking a bit more at the code, I think I know where's the issue... gonna test tomorrow to confirm.
JColeGeotab commentedon Apr 12, 2023
Nope, the threads stuck in that stack are the only threads stuck in Npgsql code.
vonzshik commentedon Apr 12, 2023
Yeah, OK. The logic there is, that we first reset
mres, then register a callback onCancellationToken, and then read from the socket (and settingmresat some point). Now, registering a callback onCancellationTokencan immediately trigger that callback synchronously in case ifCancellationTokenis already cancelled. So I guess that's what happens. The thread which is supposed to read from the socket actually stuck onmres.JColeGeotab commentedon Apr 12, 2023
If it helps, there were a few threads that had a slightly different stack.
In the dotnet-stack output we captured from a process with 446 blocked threads, there were 16 with the stack I pasted above, and 430 with the shorter stack below, missing the
StartNestedCancellableOperationline.Diff:

vonzshik commentedon Apr 12, 2023
The stack might be different due to jit recompiling that method from tier 0 (fast compilation, minimum of optimizations) to tier 1 (longer compilation, more optimization options, like method inlining). But looking at the stack trace it does indeed confirm that I was right. If the callback isn't called synchronously, we would have gotten a stack trace like this:
Fix deadlock while cancelling a query
Fix deadlock while cancelling a query
Fix deadlock while cancelling a query (#5033)
Fix deadlock while cancelling a query (#5033)
Fix deadlock while cancelling a query (#5033)
Fix deadlock while cancelling a query (#5033)
JColeGeotab commentedon Apr 24, 2023
Thanks for the quick turnaround! As soon as 7.0.4 is available we should be able to quickly confirm that our issue is resolved.
vonzshik commentedon Apr 24, 2023
@JColeGeotab you can also test using myget feed.