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 }}
Repository navigation
Double Disposal of NpgsqlTransaction can cause wrong connection to be closed / Connection Pool Exhaustion #6665
We use a wrapper for NpgsqlTransaction that wraps a transaction/connection pair. In this model, the NpgsqlConnection we acquire lives as long as the transaction. That is, when we commit or rollback the transaction, we also dispose the connection object.
A recent change in our code ensured that the transaction and the connection are closed before calling a post commit hook.
"SslStream disposed" and "Operation canceled" errors while reading
Depending on which exact race scenario was hit. After a while, we've experienced the connection pool being exhausted.
Cause
Recycling of NpgsqlTransaction introduced in version 5 via #2416
IMO, the behavior of "undisposing" an object is unexpected - quite special, so to say.
I would like to cite:
(...) a Dispose method should be idempotent, such that it's callable multiple times without throwing an exception. Furthermore, subsequent invocations of Dispose should do nothing.
I understand some of the reasons why this guidance exists, but in no way am promising a complete picture:
it can be tricky to get single disposal right, especially when you pass on a disposable object A to another disposable object B, and it's unclear whether B.Dispose() will call A.Dispose() or not. Like: Does the reader dispose the file handle / connection?
historically, in .net framework (a long time ago), some code involving yield return and using could cause double disposal, even if you wrote the code a 100% correct.
My personal stance on this is, that I keep my own disposal code simple. I always trust in the underlying objects and primitives to do their jobs right, I never do isDisposed checks (unless I'm the lowest level of the chain). Until NpgsqlTransaction I never had a problem with this pattern.
The work around is in our code is rather obvious: *do introduce a bool isDisposed check, and prevent double disposal.
It's also not a big thing. I'm mostly reporting because it could cause problems for others, too. And, especially race conditions, can be difficult to diagnose.
When this was introduced in #2416, the reason given was performance gain.
The original perf-gain has not been specified with any numbers. Maybe it has become insignificant in the meantime? If so, I suppose the implementation would be simpler w/o reuse, too.
Alternatively, maybe the following pattern could be considered:
renaming the current NpgsqlTransaction to ReusableTransaction (and make it internal)
expose NgsqlTransaction as a wrapper around ReusableTransaction:
publicclassNpgsqlTransaction:DbTransaction{ReusableTransaction_reusable;internalNpgsqlTransaction(ReusableTransactionreusable){_reusable=reusable;}publicvoidDispose(){_reusable?.Dispose();_reusable=null;}ReusableTransactionReusable=>_reusable??thrownewObjectDisposedException("transaction has already been disposed");// example of actually doing something with the transaction.publicoverridevoidSave(stringname){Reusable.Save(name);}}
I was able to reproduce this. With a pool of one connection, the next BeginTransaction returned the same disposed transaction, and disposing it again rolled that later transaction back. I have a PR ready that addresses it: #6670
Activity
sanjaysingh commentedon Oct 14, 2026
I was able to reproduce this. With a pool of one connection, the next BeginTransaction returned the same disposed transaction, and disposing it again rolled that later transaction back. I have a PR ready that addresses it: #6670