Skip to content
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

Fix concurrency issues on TransactionScope timeout #3483

Merged
merged 22 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f245b8f
Add a test case for system transactions timeout
fredericDelaporte Feb 2, 2024
7df425a
Fix concurrency issues on TransactionScope timeout
fredericDelaporte Feb 3, 2024
c1ab2eb
Fix the fix
fredericDelaporte Feb 6, 2024
df14163
Fix corrupted character
fredericDelaporte Feb 6, 2024
01e97ae
Still attempt to release the session even if in use
fredericDelaporte Feb 7, 2024
fa20807
Generate async files
github-actions[bot] Feb 7, 2024
1a71fc1
Apply review
fredericDelaporte Feb 8, 2024
440807b
Allow more control of synchronization failures
fredericDelaporte Feb 8, 2024
79b9f03
Generate async files
github-actions[bot] Feb 8, 2024
2da9e3a
Fix typos
fredericDelaporte Feb 8, 2024
53e01f7
Downgrade the log to Warn
fredericDelaporte Feb 9, 2024
e26e53e
Lock explicitly
fredericDelaporte Feb 13, 2024
9ec5821
Handle the case of concurrent session disposal
fredericDelaporte Feb 13, 2024
20ec428
Fix whitespace
fredericDelaporte Feb 21, 2024
f0fd7f4
Remove the crutch for a very unlikely concurrency possibility
fredericDelaporte Feb 21, 2024
3d54b26
Add more information on unclosed session
fredericDelaporte Feb 25, 2024
d9bcbee
Remove another concurrency issue possibility
fredericDelaporte Feb 25, 2024
78639a6
The crutch has to come back
fredericDelaporte Feb 26, 2024
06013ad
Revert "The crutch has to come back"
fredericDelaporte Feb 27, 2024
f212f1a
Avoid having a flaky SupportsTransactionTimeout test
fredericDelaporte Feb 28, 2024
d72795c
Generate async files
github-actions[bot] Feb 28, 2024
8042475
Fix teardown
fredericDelaporte Feb 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ root=true

[*]
insert_final_newline = true
charset = utf-8
hazzik marked this conversation as resolved.
Show resolved Hide resolved

[*.cs]
indent_style = tab
Expand Down
2 changes: 2 additions & 0 deletions build-common/teamcity-hibernate.cfg.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@
<property name="odbc.explicit_datetime_scale"></property>
<property name="oracle.use_n_prefixed_types_for_unicode"></property>
<property name="query.default_cast_length"></property>
<property name="transaction.ignore_session_synchronization_failures"></property>
<property name="transaction.system_completion_lock_timeout"></property>
</session-factory>
</hibernate-configuration>
20 changes: 20 additions & 0 deletions default.build
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,26 @@
<namespace prefix="hbm" uri="urn:nhibernate-configuration-2.2" />
</namespaces>
</xmlpoke>

<!-- Make sure the property exists - it's only set for some scenarios. -->
<property name="nhibernate.transaction.ignore_session_synchronization_failures" value="" unless="${property::exists('nhibernate.transaction.ignore_session_synchronization_failures')}"/>
<xmlpoke file="${app.config}"
xpath="//*/hbm:property[@name='transaction.ignore_session_synchronization_failures']"
value="${nhibernate.transaction.ignore_session_synchronization_failures}">
<namespaces>
<namespace prefix="hbm" uri="urn:nhibernate-configuration-2.2" />
</namespaces>
</xmlpoke>

<!-- Make sure the property exists - it's only set for some scenarios. -->
<property name="nhibernate.transaction.system_completion_lock_timeout" value="" unless="${property::exists('nhibernate.transaction.system_completion_lock_timeout')}"/>
<xmlpoke file="${app.config}"
xpath="//*/hbm:property[@name='transaction.system_completion_lock_timeout']"
value="${nhibernate.transaction.system_completion_lock_timeout}">
<namespaces>
<namespace prefix="hbm" uri="urn:nhibernate-configuration-2.2" />
</namespaces>
</xmlpoke>
</target>

<target name="put-connection-settings-into-app-config">
Expand Down
31 changes: 29 additions & 2 deletions doc/reference/modules/configuration.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1050,8 +1050,8 @@ var session = sessions.OpenSession(conn);
after scope disposal. This occurs when the transaction is distributed.
This notably concerns <literal>ISessionImplementor.AfterTransactionCompletion(bool, ITransaction)</literal>.
NHibernate protects the session from being concurrently used by the code following the scope disposal
with a lock. To prevent any application freeze, this lock has a default timeout of five seconds. If the
application appears to require longer (!) running transaction completion events, this setting allows to
with a lock. To prevent any application freeze, this lock has a default timeout of one second. If the
fredericDelaporte marked this conversation as resolved.
Show resolved Hide resolved
application appears to require longer running transaction completion events, this setting allows to
raise this timeout. <literal>-1</literal> disables the timeout.
</para>
<para>
Expand All @@ -1060,6 +1060,33 @@ var session = sessions.OpenSession(conn);
</para>
</entry>
</row>
<row>
<entry>
<literal>transaction.ignore_session_synchronization_failures</literal>
</entry>
<entry>
Whether session synchronisation failures occuring during finalizations of system transaction should be
ignored or not. <literal>false</literal> by default.
<para>
When a system transaction terminates abnormaly, especially through timeouts, it may have its
completion events running on concurrent threads while the session is still performing some processing.
To prevent threading concurrency failures, NHibernate then wait for the session to end its processing,
up to <literal>transaction.system_completion_lock_timeout</literal>. If the session processing is still ongoing
afterwards, it will by default log an error, perform transaction finalization processing concurrently,
then throw a synchronization error. This setting allows to disable that later throw.
</para>
<para>
Disabling the throw can be useful if the used data provider has its own locking mechanism applied
during transaction completion, preventing the session to end its processing. It may then be safe to
ignore this synchronization failure. In case of threading concurrency failure, you may then need to
raise <literal>transaction.system_completion_lock_timeout</literal>.
</para>
<para>
<emphasis role="strong">eg.</emphasis>
<literal>true</literal> | <literal>false</literal>
</para>
</entry>
</row>
<row>
<entry>
<literal>transaction.auto_join</literal>
Expand Down
2 changes: 2 additions & 0 deletions psake.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Task Set-Configuration {
'connection.connection_string' = 'Server=(local)\SQL2017;Uid=sa;Pwd=Password12!;Database=nhibernateOdbc;Driver={SQL Server Native Client 11.0};Mars_Connection=yes;';
'connection.driver_class' = 'NHibernate.Driver.OdbcDriver';
'odbc.explicit_datetime_scale' = '3';
'transaction.ignore_session_synchronization_failures' = 'true';
'transaction.system_completion_lock_timeout' = '200';
<# We need to use a dialect that avoids mapping DbType.Time to TIME on MSSQL. On modern SQL Server
this becomes TIME(7). Later, such values cannot be read back over ODBC. The
error we get is "System.ArgumentException : Unknown SQL type - SS_TIME_EX.". I don't know for certain
Expand Down
2 changes: 2 additions & 0 deletions src/NHibernate.Config.Templates/SapSQLAnywhere.cfg.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ for your own use before compiling tests in Visual Studio.
</property>
<property name="dialect">NHibernate.Dialect.SybaseSQLAnywhere12Dialect</property>
<property name="query.substitutions">true=1;false=0</property>
<property name="transaction.ignore_session_synchronization_failures">true</property>
<property name="transaction.system_completion_lock_timeout">200</property>
</session-factory>
</hibernate-configuration>
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@


using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Expand All @@ -30,6 +31,13 @@ public class SystemTransactionFixtureAsync : SystemTransactionFixtureBase
protected override bool UseConnectionOnSystemTransactionPrepare => true;
protected override bool AutoJoinTransaction => true;

protected override void OnTearDown()
{
base.OnTearDown();
// The SupportsTransactionTimeout test may change this, restore it to its default value.
FailOnNotClosedSession = true;
}

[Test]
public async Task WillNotCrashOnPrepareFailureAsync()
{
Expand Down Expand Up @@ -524,6 +532,152 @@ public async Task EnforceConnectionUsageRulesOnTransactionCompletionAsync()
// Currently always forbidden, whatever UseConnectionOnSystemTransactionEvents.
Assert.That(interceptor.AfterException, Is.TypeOf<HibernateException>());
}

// This test check a concurrency issue hard to reproduce. If it is flaky, it has to be considered failing.
// In such case, raise triesCount to investigate it locally with more chances of triggering the trouble.
[Test]
public async Task SupportsTransactionTimeoutAsync()
{
Assume.That(TestDialect.SupportsTransactionScopeTimeouts, Is.True, "The tested dialect is not supported for transaction scope timeouts.");
// Other special cases: ODBC and SAP SQL Anywhere succeed this test only with transaction.ignore_session_synchronization_failures
// enabled.
// They freeze the session during the transaction cancellation. To avoid the test to be very long, the synchronization
// lock timeout should be lowered too.

// A concurrency issue exists with the legacy setting allowing to use the session from transaction completion, which
// may cause session leaks. Ignore them.
FailOnNotClosedSession = !UseConnectionOnSystemTransactionPrepare;

// Test case adapted from https://github.com/kaksmet/NHibBugRepro

// Create some test data.
const int entitiesCount = 5000;
using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
for (var i = 0; i < entitiesCount; i++)
{
var person = new Person
{
NotNullData = Guid.NewGuid().ToString()
};

await (s.SaveAsync(person));
}

await (t.CommitAsync());
}

// Setup unhandled exception catcher.
_unhandledExceptions = new ConcurrentBag<object>();
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
try
{
// Generate transaction timeouts.
const int triesCount = 100;
var txOptions = new TransactionOptions { Timeout = TimeSpan.FromMilliseconds(1) };
var timeoutsCount = 0;
for (var i = 0; i < triesCount; i++)
{
try
{
using var txScope = new TransactionScope(TransactionScopeOption.Required, txOptions, TransactionScopeAsyncFlowOption.Enabled);
using var session = OpenSession();
var data = await (session.CreateCriteria<Person>().ListAsync());
Assert.That(data, Has.Count.EqualTo(entitiesCount), "Unexpected count of loaded entities.");
await (Task.Delay(2));
var count = await (session.Query<Person>().CountAsync());
Assert.That(count, Is.EqualTo(entitiesCount), "Unexpected entities count.");
txScope.Complete();
}
catch
{
// Assume that is a transaction timeout. It may cause various failures, of which some are hard to identify.
timeoutsCount++;
}
// If in need of checking some specific failures, the following code may be used instead:
/*
catch (Exception ex)
{
var currentEx = ex;
// Depending on where the transaction aborption has broken NHibernate processing, we may
// get various exceptions, like directly a TransactionAbortedException with an inner
// TimeoutException, or a HibernateException encapsulating a TransactionException with a
// timeout, ...
bool isTransactionException, isTimeout;
do
{
isTransactionException = currentEx is System.Transactions.TransactionException;
isTimeout = isTransactionException && currentEx is TransactionAbortedException;
currentEx = currentEx.InnerException;
}
while (!isTransactionException && currentEx != null);
while (!isTimeout && currentEx != null)
{
isTimeout = currentEx is TimeoutException;
currentEx = currentEx?.InnerException;
}

if (!isTimeout)
{
// We may also get a GenericADOException with an InvalidOperationException stating the
// transaction associated to the connection is no more active but not yet suppressed,
// and that for executing some SQL, we need to suppress it. That is a weak way of
// identifying the case, especially with the many localizations of the message.
currentEx = ex;
do
{
isTimeout = currentEx is InvalidOperationException && currentEx.Message.Contains("SQL");
currentEx = currentEx?.InnerException;
}
while (!isTimeout && currentEx != null);
}

if (isTimeout)
timeoutsCount++;
else
throw;
}
*/
}

Assert.That(
_unhandledExceptions.Count,
Is.EqualTo(0),
"Unhandled exceptions have occurred: {0}",
string.Join(@"

", _unhandledExceptions));

// Despite the Thread sleep and the count of entities to load, this test may get the timeout only for slightly
// more than 10% of the attempts.
Warn.Unless(timeoutsCount, Is.GreaterThan(5), "The test should have generated more timeouts.");
}
finally
{
AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException;
}
}

private ConcurrentBag<object> _unhandledExceptions;

private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception exception)
{
// Ascertain NHibernate is involved. Some unhandled exceptions occur due to the
// TransactionScope timeout operating on an unexpected thread for the data provider.
var isNHibernateInvolved = false;
while (exception != null && !isNHibernateInvolved)
{
isNHibernateInvolved = exception.StackTrace != null && exception.StackTrace.ToLowerInvariant().Contains("nhibernate");
exception = exception.InnerException;
}
if (!isNHibernateInvolved)
return;
}
_unhandledExceptions.Add(e.ExceptionObject);
}
}

[TestFixture]
Expand Down
Loading
Loading