-
Notifications
You must be signed in to change notification settings - Fork 934
[WIP] Try fix ODP.NET DTC issues #532
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
Conversation
387c75d
to
638f148
Compare
@hazzik What is the status of your pull request? We have this issue right now:
Do you think your pull request could solve that kind of issue? |
…eader after it was disposed.
@lnu can you help me understand which connection leaks? I have oracle only in our test agent, and it's extremely hard to run memory profiling there. |
I only have access to oracle at work and won't be there before next year. A simple repro should be to throw an exception inside a TransactionScope. By default the max pool is 100. If I loop 100 times and throw an exception each time the pool should be exhausted and you'll receive this exception: pooled connection request timeout. I'll try to do a repro. ps: it occurs only with oracle, with sql server the connection is closed as expected(99% sure) |
@hazzik It seems a lot better but from time to time I get an ObjectDisposedException If it occurs multiple times it leads to a pool exhaust. |
thanks a lot, @lnu. Can you please show the full stack trace of the exception? |
@hazzik I ran more tests(1 handler with 4 threads, 5000 messsages with 5 retries and 2 slr) and no exception. I've also configured odp.net tracing and the connection are opened and close correclty(in this case max 9 connections at peak). I guess I had the exception only when debugging with visual studio and having some breakpoints. |
@lnu, I tend to think that the way NHibernate enlist session into ambient transaction is the major cause of the trouble. May you try to reproduce the trouble with a modified transaction factory to validate that point? The transaction factory can be change with configuration setting // Unfortunately, cannot derive and override NHibernate impl, methods are not virtual.
public class CustomAdoNetTransactionFactory : ITransactionFactory
{
private readonly AdoNetTransactionFactory _adoNetTransactionFactory =
new AdoNetTransactionFactory();
private readonly ConcurrentDictionary<DbConnection, System.Transactions.Transaction> _sessionsTransaction =
new ConcurrentDictionary<DbConnection, System.Transactions.Transaction>();
public void Configure(IDictionary props) { }
public ITransaction CreateTransaction(ISessionImplementor session)
{
return new AdoTransaction(session);
}
public void EnlistInDistributedTransactionIfNeeded(ISessionImplementor session)
{
// No session enlistment. This disables automatic flushes before ambient transaction
// commits. Explicit Flush calls required.
// Still make sure the session connection is enlisted, in case it was acquired before
// transaction scope start.
// Will not support nested transaction scope. (Will throw, while current NHibernate
// just stay in previous scope.)
// Will cause an "earlier than required" connection acquisition.
// It is required to enlist with null when the scope is ended, otherwise using
// the transaction without a new scope will fail by attempting to use it inside
// the completed scope.
// If an explicit transaction is ongoing, we must not enlist. We should not enlist
// either if the connection was supplied by user (let him handle that in such case),
// but there are currently no ways to know this from here.
if (!session.ConnectionManager.Transaction.IsActive)
{
// Enlist is called terribly frequently, and in some circumstances, it will
// not support to be called with the same value. So track what was the previous
// call and do not call it again if unneeded.
// (And Sql/OleDb/Odbc/Oracle manage/PostgreSql/MySql/Firebird/SQLite connections
// support multiple calls with the same ongoing transaction, but some others may not.)
var current = GetCurrentTransaction();
var connection = session.Connection;
System.Transactions.Transaction previous;
if (!_sessionsTransaction.TryGetValue(connection, out previous) || previous != current)
{
_sessionsTransaction.AddOrUpdate(connection, current, (s, t) => current);
if (current == null &&
// This will need an ad-hoc property on Dialect base class instead.
(session.Factory.Dialect is SQLiteDialect || session.Factory.Dialect is MsSqlCeDialect))
{
// Some connections does not support enlisting with null
// Let them with their previous transaction if any, the application
// will fail if the connection was left with a completed transaction due to this.
return;
}
session.Connection.EnlistTransaction(current);
}
}
}
public bool IsInDistributedActiveTransaction(ISessionImplementor session)
{
// Avoid agressive connection release while a transaction is ongoing. Allow
// auto-flushes (flushes before queries on dirtied entities).
return GetCurrentTransaction() != null;
}
public void ExecuteWorkInIsolation(ISessionImplementor session, IIsolatedWork work,
bool transacted)
{
using (var tx = new TransactionScope(TransactionScopeOption.Suppress))
{
// instead of duplicating the logic, we suppress the DTC transaction
// and create our own transaction instead
_adoNetTransactionFactory.ExecuteWorkInIsolation(session, work,
transacted);
tx.Complete();
}
}
private System.Transactions.Transaction GetCurrentTransaction()
{
try
{
return System.Transactions.Transaction.Current;
}
catch (InvalidOperationException)
{
// This damn thing may yield an invalid operation exception (instead of null
// or of a completed transaction) if we are between scope.Complete() and
// scope.Dispose(). This happen when having completed the scope then disposing
// the session and only after that disposing the scope.
// Instead of testing System.Transactions.Transaction.Current here, storing in
// connection manager the ambient transaction associated to the connection
// (and updating it when enlisting) then checking that stored transaction would
// reduce testes on Transaction.Current.
return null;
}
}
} The major consequence of this change will be that all sessions will need to be explicitly flushed before being disposed or before the transaction is committed, whichever come first. Otherwise, the changes will be lost (or done outside of a transaction if flushed after transaction commit). I hope your app can accommodate with this. If the application opens a session before a transaction scope, causes it to acquire a connection, then opens a transaction scope, the connection may not participate in it. But this is unlikely to occur, unless the connection release mode is Using this class could be a workaround for the trouble, without having to compile your own NHibernate version or wait for a next release. |
If you use second level cache, this transaction factory will cause it to be disabled when data is modified. |
I think it worth make a test PR to verify your assumptions
|
Maybe only as a test coded in test project. I do not think adding this as a new transaction factory in the core would be suitable "as is". Will do. |
I have made a first test within #410. |
By using |
It means everything happening inside this suppressed transaction scope will be outside the previously ambiant transaction. But once this suppressed transaction scope ends, the previous ambient transaction comes back. I guess you refer to
Is this a statement or a question? With current NHibernate code (at least v3 and v4), you were not supposed to need explicit flushing before transaction scope completion with default flush mode ( With the modified transaction factory, flushing before transaction scope completion (or before session disposal) is required for changes to be persisted, provided the transaction scope ends up committed.
Under the hood the session connection is still enlisted in the transaction, since it is acquired within the transaction scope. So your operation on the session are transacted. The lack of session enlistment just mean there will be no more auto-flushing from transaction scope end.
And its behavior further depends on the used driver (managed or not). |
Now it works for all db in #410, for NH-2176 test. I am going to test it with other transaction scope tests. Code above updated. |
cd4bb54
to
e145322
Compare
Sorry I read your first comment a bit too fast. |
@lnu, no need to try the custom transaction factory, it suffers from a major roadblock: some internal checks forbid actually closing a session within a transaction scope. The current transaction factory does not suffer from it because it hijacks the session dispose for preventing it to actually close the session, then close it only at transaction completion. The custom factory cease doing that, and cannot prevent the check forbidding closing the session within a transaction scope to get triggered. This check is done by |
Close in favor of #627 |
No description provided.