Open Lexy2 opened 10 months ago
Hey @Lexy2 !
Thank you so much for that write up. That's a crazy deep dive, I love it. And the explanation makes total sense. I didn't go that deep when I hit it because was trying to bring production online, and didn't go back.
I'll definitely credit you in future iterations of my talks!
Hey @DamianMac,
Sorry about this, but, in absence of better communication channels, I decided to reach out to you here, especially because the issue is directly related to this repo.
[The day before] Yesterday you warned us to always use
ToList()
for Marten queries before returning the result in an ASP.NET Core API controller. Because if you don't, you'll quickly deplete the PgSql connection pool.That interested a few people at the meeting, and I was in that crowd. So I decided to dive deeper in the issue, and wanted to present you with my findings.
Scope
.Query()
method with any LINQ traits supported by Marten, is just an expression tree. The object that it returns - anIMartenQueryable
, does not do anything at all until it's enumerated.IMartenQueryable
object connects to the PgSql database only when it's enumerated. Specifically, a call toGetEnumerator
opens the PgSql database connection through call stackObjectDisposedException
when we try to access the query outside of the session scope? As it's inusing
, it should be disposed when we exit the block?Explanation
Well, the answer to that is a combination of bugs / features in
NpgsqlConnection
andMartenControlledConnectionTransaction
objects 😜.That
EnsureConnected
method looks like this:So what happens in sync code is
NpgsqlConnection
in itsDispose
method only closes the connection, but doesn't do anything really if the connection has not been opened.IEnumerable
is serialized bySystem.Text.Json
- see ASP.NET Core Best Practices and thus enumerated:_disposed
flag of the query is already set in #2!NpgsqlConnection
object and the underlying objects remain alive forever, keeping the connection open and depleting the pool.Workaround
If you call
GetEnumerator
within theusing
block, the disposal works as expected, and trying to enumerate the collection outside the session scope rightfully throwsBasically, you just add this line:
and the code behaves as it should.
Async
Now there was also a question, is the behaviour the same in async code? Kind of, yes.
Returning an
IEnumerable
asynchronously in an asynchronous session (invoked byQuerySerializableSessionAsync
) throws properly. Returning anIAsyncEnumerable
is different. Just callingToAsyncEnumerable
and asynchronously enumerating the response outside of the session scope results in a similar situation.However, unlike the sync version of the code, this behaviour is documented in Querying to IAsyncEnumerable:
Hope
The dreaded
MartenControlledConnectionTransaction
class does not exist in themaster
branch of the current Marten repo.I tried using
7.0.0-beta.5
version of Marten, the connection no longer stays open in an ASP.NET Core application, even if you don't callToList()
within the session scope, and the result is returned to the caller. Thus, when 7.0.0 is released, this mistake won't cause such big problems.Hope it was helpful 😎