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(); + } + } } ///