diff --git a/src/NHibernate.Test/Async/Stateless/StatelessSessionCancelQueryFixture.cs b/src/NHibernate.Test/Async/Stateless/StatelessSessionCancelQueryFixture.cs
new file mode 100644
index 00000000000..b89528ef58c
--- /dev/null
+++ b/src/NHibernate.Test/Async/Stateless/StatelessSessionCancelQueryFixture.cs
@@ -0,0 +1,120 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by AsyncGenerator.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+
+using System;
+using System.Data;
+using System.Data.SqlClient;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using NHibernate.Linq;
+using NUnit.Framework;
+
+namespace NHibernate.Test.Stateless
+{
+ [TestFixture]
+ public class StatelessSessionCancelQueryFixtureAsync : TestCase
+ {
+ protected override string MappingsAssembly
+ {
+ get { return "NHibernate.Test"; }
+ }
+
+ protected override string[] Mappings
+ {
+ get { return new[] { "Stateless.Document.hbm.xml" }; }
+ }
+
+ private const string _documentName = "SomeDocument";
+ private CultureInfo _backupCulture;
+ private CultureInfo _backupUICulture;
+
+ protected override void OnSetUp()
+ {
+ if (CultureInfo.CurrentCulture.TwoLetterISOLanguageName != CultureInfo.InvariantCulture.TwoLetterISOLanguageName)
+ {
+ // This test needs to run in English
+ _backupCulture = CultureInfo.CurrentCulture;
+ _backupUICulture = CultureInfo.CurrentUICulture;
+ CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
+ CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
+ }
+
+ using (var s = Sfi.OpenStatelessSession())
+ using (var t = s.BeginTransaction())
+ {
+ s.Insert(new Document("Some text", _documentName));
+ t.Commit();
+ }
+ }
+
+ protected override void OnTearDown()
+ {
+ using (var s = Sfi.OpenStatelessSession())
+ using (var t = s.BeginTransaction())
+ {
+ s.CreateQuery("delete Document").ExecuteUpdate();
+ t.Commit();
+ }
+
+ if (_backupCulture != null)
+ {
+ CultureInfo.CurrentCulture = _backupCulture;
+ CultureInfo.CurrentUICulture = _backupUICulture;
+ }
+ }
+
+ protected override bool AppliesTo(Dialect.Dialect dialect)
+ {
+ return TestDialect.SupportsCancelQuery &&
+ TestDialect.SupportsSelectForUpdate;
+ }
+
+ private async Task CancelQueryTestAsync(Action queryAction, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ using (var s1 = Sfi.OpenStatelessSession())
+ using (var t1 = s1.BeginTransaction())
+ {
+ await (s1.GetAsync(_documentName, LockMode.Upgrade, cancellationToken));
+
+ using (var s2 = Sfi.OpenStatelessSession())
+ using (var t2 = s2.BeginTransaction())
+ {
+ var queryTask = Task.Factory.StartNew(() => queryAction(s2));
+
+ await (Task.Delay(200, cancellationToken));
+ s2.CancelQuery();
+ Assert.That(() => queryTask,
+ Throws.InnerException.TypeOf(typeof(OperationCanceledException))
+ .Or.InnerException.Message.Contains("cancel"));
+ }
+ }
+ }
+
+ [Test]
+ public async Task CancelHqlQueryAsync()
+ {
+ await (CancelQueryTestAsync(s => s.CreateQuery("from Document d").SetLockMode("d", LockMode.Upgrade).List()));
+ }
+
+ [Test]
+ public async Task CancelLinqQueryAsync()
+ {
+ await (CancelQueryTestAsync(s => s.Query().WithLock(LockMode.Upgrade).ToList()));
+ }
+
+ [Test]
+ public async Task CancelQueryOverQueryAsync()
+ {
+ await (CancelQueryTestAsync(s => s.QueryOver().Lock().Upgrade.List()));
+ }
+ }
+}
diff --git a/src/NHibernate.Test/Stateless/StatelessSessionCancelQueryFixture.cs b/src/NHibernate.Test/Stateless/StatelessSessionCancelQueryFixture.cs
new file mode 100644
index 00000000000..a86a430826b
--- /dev/null
+++ b/src/NHibernate.Test/Stateless/StatelessSessionCancelQueryFixture.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Data;
+using System.Data.SqlClient;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using NHibernate.Linq;
+using NUnit.Framework;
+
+namespace NHibernate.Test.Stateless
+{
+ [TestFixture]
+ public class StatelessSessionCancelQueryFixture : TestCase
+ {
+ protected override string MappingsAssembly
+ {
+ get { return "NHibernate.Test"; }
+ }
+
+ protected override string[] Mappings
+ {
+ get { return new[] { "Stateless.Document.hbm.xml" }; }
+ }
+
+ private const string _documentName = "SomeDocument";
+ private CultureInfo _backupCulture;
+ private CultureInfo _backupUICulture;
+
+ protected override void OnSetUp()
+ {
+ if (CultureInfo.CurrentCulture.TwoLetterISOLanguageName != CultureInfo.InvariantCulture.TwoLetterISOLanguageName)
+ {
+ // This test needs to run in English
+ _backupCulture = CultureInfo.CurrentCulture;
+ _backupUICulture = CultureInfo.CurrentUICulture;
+ CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
+ CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
+ }
+
+ using (var s = Sfi.OpenStatelessSession())
+ using (var t = s.BeginTransaction())
+ {
+ s.Insert(new Document("Some text", _documentName));
+ t.Commit();
+ }
+ }
+
+ protected override void OnTearDown()
+ {
+ using (var s = Sfi.OpenStatelessSession())
+ using (var t = s.BeginTransaction())
+ {
+ s.CreateQuery("delete Document").ExecuteUpdate();
+ t.Commit();
+ }
+
+ if (_backupCulture != null)
+ {
+ CultureInfo.CurrentCulture = _backupCulture;
+ CultureInfo.CurrentUICulture = _backupUICulture;
+ }
+ }
+
+ protected override bool AppliesTo(Dialect.Dialect dialect)
+ {
+ return TestDialect.SupportsCancelQuery &&
+ TestDialect.SupportsSelectForUpdate;
+ }
+
+ private void CancelQueryTest(Action queryAction)
+ {
+ using (var s1 = Sfi.OpenStatelessSession())
+ using (var t1 = s1.BeginTransaction())
+ {
+ s1.Get(_documentName, LockMode.Upgrade);
+
+ using (var s2 = Sfi.OpenStatelessSession())
+ using (var t2 = s2.BeginTransaction())
+ {
+ var queryTask = Task.Factory.StartNew(() => queryAction(s2));
+
+ Thread.Sleep(200);
+ s2.CancelQuery();
+ Assert.That(() => queryTask,
+ Throws.InnerException.TypeOf(typeof(OperationCanceledException))
+ .Or.InnerException.Message.Contains("cancel"));
+ }
+ }
+ }
+
+ [Test]
+ public void CancelHqlQuery()
+ {
+ CancelQueryTest(s => s.CreateQuery("from Document d").SetLockMode("d", LockMode.Upgrade).List());
+ }
+
+ [Test]
+ public void CancelLinqQuery()
+ {
+ CancelQueryTest(s => s.Query().WithLock(LockMode.Upgrade).ToList());
+ }
+
+ [Test]
+ public void CancelQueryOverQuery()
+ {
+ CancelQueryTest(s => s.QueryOver().Lock().Upgrade.List());
+ }
+ }
+}
diff --git a/src/NHibernate.Test/TestDialect.cs b/src/NHibernate.Test/TestDialect.cs
index 4e77c7e0460..49b2f7e0f69 100644
--- a/src/NHibernate.Test/TestDialect.cs
+++ b/src/NHibernate.Test/TestDialect.cs
@@ -198,5 +198,10 @@ public bool SupportsSqlType(SqlType sqlType)
/// Returns true if you can modify the same table which you use in the SELECT part.
///
public virtual bool SupportsModifyAndSelectSameTable => true;
+
+ ///
+ /// Returns true if you can cancel a query.
+ ///
+ public virtual bool SupportsCancelQuery => true;
}
}
diff --git a/src/NHibernate.Test/TestDialects/MsSql2008TestDialect.cs b/src/NHibernate.Test/TestDialects/MsSql2008TestDialect.cs
index bf4c375e761..ec1d00fb563 100644
--- a/src/NHibernate.Test/TestDialects/MsSql2008TestDialect.cs
+++ b/src/NHibernate.Test/TestDialects/MsSql2008TestDialect.cs
@@ -1,4 +1,6 @@
-namespace NHibernate.Test.TestDialects
+using System.Runtime.InteropServices;
+
+namespace NHibernate.Test.TestDialects
{
public class MsSql2008TestDialect : TestDialect
{
@@ -11,5 +13,9 @@ public MsSql2008TestDialect(Dialect.Dialect dialect)
/// Does not support SELECT FOR UPDATE with paging
///
public override bool SupportsSelectForUpdateWithPaging => false;
+
+ ///
+ /// Canceling a query hangs under Linux with Sql2008ClientDriver. (It may be a data provider bug fixed with MicrosoftDataSqlClientDriver.)
+ public override bool SupportsCancelQuery => !RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
}
}
diff --git a/src/NHibernate.Test/TestDialects/Oracle10gTestDialect.cs b/src/NHibernate.Test/TestDialects/Oracle10gTestDialect.cs
index dcedb6b60ac..4a5fb8d2322 100644
--- a/src/NHibernate.Test/TestDialects/Oracle10gTestDialect.cs
+++ b/src/NHibernate.Test/TestDialects/Oracle10gTestDialect.cs
@@ -1,4 +1,6 @@
-namespace NHibernate.Test.TestDialects
+using System.Runtime.InteropServices;
+
+namespace NHibernate.Test.TestDialects
{
public class Oracle10gTestDialect : TestDialect
{
@@ -12,5 +14,9 @@ public Oracle10gTestDialect(Dialect.Dialect dialect) : base(dialect)
public override bool SupportsSelectForUpdateWithPaging => false;
public override bool SupportsAggregateInSubSelect => true;
+
+ ///
+ /// Canceling a query hangs under Linux with OracleManagedDataClientDriver 21.6.1.
+ public override bool SupportsCancelQuery => !RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
}
}
diff --git a/src/NHibernate/AdoNet/AbstractBatcher.cs b/src/NHibernate/AdoNet/AbstractBatcher.cs
index 520b4e152a3..99522150a2a 100644
--- a/src/NHibernate/AdoNet/AbstractBatcher.cs
+++ b/src/NHibernate/AdoNet/AbstractBatcher.cs
@@ -253,6 +253,11 @@ private DbDataReader DoExecuteReader(DbCommand cmd)
try
{
var reader = cmd.ExecuteReader();
+ if (reader == null)
+ {
+ // MySql may return null instead of an exception, by example when the query is canceled by another thread.
+ throw new InvalidOperationException("The query execution has yielded a null reader. (Has it been canceled?)");
+ }
return _factory.ConnectionProvider.Driver.SupportsMultipleOpenReaders
? reader
: NHybridDataReader.Create(reader);
diff --git a/src/NHibernate/Async/AdoNet/AbstractBatcher.cs b/src/NHibernate/Async/AdoNet/AbstractBatcher.cs
index ed60b6c704c..21168ef3ea6 100644
--- a/src/NHibernate/Async/AdoNet/AbstractBatcher.cs
+++ b/src/NHibernate/Async/AdoNet/AbstractBatcher.cs
@@ -168,6 +168,11 @@ private async Task DoExecuteReaderAsync(DbCommand cmd, Cancellatio
try
{
var reader = await (cmd.ExecuteReaderAsync(cancellationToken)).ConfigureAwait(false);
+ if (reader == null)
+ {
+ // MySql may return null instead of an exception, by example when the query is canceled by another thread.
+ throw new InvalidOperationException("The query execution has yielded a null reader. (Has it been canceled?)");
+ }
return _factory.ConnectionProvider.Driver.SupportsMultipleOpenReaders
? reader
: await (NHybridDataReader.CreateAsync(reader, cancellationToken)).ConfigureAwait(false);
diff --git a/src/NHibernate/IStatelessSession.cs b/src/NHibernate/IStatelessSession.cs
index 66d6bc0c34d..aa64e0188a9 100644
--- a/src/NHibernate/IStatelessSession.cs
+++ b/src/NHibernate/IStatelessSession.cs
@@ -74,6 +74,22 @@ public static void FlushBatcher(this IStatelessSession session)
{
session.GetSessionImplementation().Flush();
}
+
+ ///
+ /// Cancel execution of the current query.
+ ///
+ ///
+ /// May be called from one thread to stop execution of a query in another thread.
+ /// Use with care!
+ ///
+ public static void CancelQuery(this IStatelessSession session)
+ {
+ var implementation = session.GetSessionImplementation();
+ using (implementation.BeginProcess())
+ {
+ implementation.Batcher.CancelLastQuery();
+ }
+ }
}
///