This is an automated email from the ASF dual-hosted git repository. ptupitsyn pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/ignite.git
The following commit(s) were added to refs/heads/master by this push: new 0aceb36 IGNITE-13214 .NET: Fix TransactionScope for read operations 0aceb36 is described below commit 0aceb369c0097dadf94fae75456521baf678005a Author: Pavel Tupitsyn <ptupit...@apache.org> AuthorDate: Thu Jul 9 21:40:10 2020 +0300 IGNITE-13214 .NET: Fix TransactionScope for read operations * Add `StartTxIfNeeded` to all cache operations (it was missing for Get, GetAll, TryGet) * Add `OptimisticTransactionTest` with explicit and ambient tests * Add exception mapping for `IgniteTxOptimisticCheckedException` * Fix error handling in ambient transactions - close Ignite tx on Prepare failure --- .../Apache.Ignite.Core.Tests.DotNetCore.csproj | 3 + .../Apache.Ignite.Core.Tests.csproj | 1 + .../Cache/CacheAbstractTransactionalTest.cs | 55 ++++++++- .../Cache/OptimisticTransactionTest.cs | 134 +++++++++++++++++++++ .../Cache/Platform/PlatformCacheTest.cs | 1 + .../Apache.Ignite.Core/Impl/Cache/CacheImpl.cs | 12 ++ .../Apache.Ignite.Core/Impl/ExceptionUtils.cs | 1 + .../Impl/Transactions/CacheTransactionManager.cs | 24 ++-- 8 files changed, 219 insertions(+), 12 deletions(-) diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests.DotNetCore/Apache.Ignite.Core.Tests.DotNetCore.csproj b/modules/platforms/dotnet/Apache.Ignite.Core.Tests.DotNetCore/Apache.Ignite.Core.Tests.DotNetCore.csproj index b835db5..e745e90 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests.DotNetCore/Apache.Ignite.Core.Tests.DotNetCore.csproj +++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests.DotNetCore/Apache.Ignite.Core.Tests.DotNetCore.csproj @@ -129,6 +129,9 @@ </Compile> <Compile Include="..\Apache.Ignite.Core.Tests\Cache\NonSerializableCacheEntryProcessor.cs" Link="Cache\NonSerializableCacheEntryProcessor.cs" /> <Compile Include="..\Apache.Ignite.Core.Tests\Cache\NonSerializableException.cs" Link="Cache\NonSerializableException.cs" /> + <Compile Include="..\Apache.Ignite.Core.Tests\Cache\OptimisticTransactionTest.cs"> + <Link>Cache\OptimisticTransactionTest.cs</Link> + </Compile> <Compile Include="..\Apache.Ignite.Core.Tests\Cache\PersistenceTest.cs" Link="Cache\PersistenceTest.cs" /> <Compile Include="..\Apache.Ignite.Core.Tests\Cache\Platform\FailingCacheStore.cs"> <Link>Cache\Platform\FailingCacheStore.cs</Link> diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj index bc1aedc..9779dda 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj +++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj @@ -117,6 +117,7 @@ <Compile Include="Cache\DataRegionMetricsTest.cs" /> <Compile Include="Cache\DataStorageMetricsTest.cs" /> <Compile Include="Cache\NearCacheTest.cs" /> + <Compile Include="Cache\OptimisticTransactionTest.cs" /> <Compile Include="Cache\Platform\PlatformCacheTestCreateDestroy.cs" /> <Compile Include="Cache\Platform\PlatformCacheTopologyChangeTest.cs" /> <Compile Include="Cache\Platform\PlatformCacheTest.cs" /> diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/CacheAbstractTransactionalTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/CacheAbstractTransactionalTest.cs index 2956669..7cf5117 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/CacheAbstractTransactionalTest.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/CacheAbstractTransactionalTest.cs @@ -347,7 +347,7 @@ namespace Apache.Ignite.Core.Tests.Cache Assert.IsTrue(tx.StartTime.Ticks > 0); Assert.AreEqual(tx.NodeId, GetIgnite(0).GetCluster().GetLocalNode().Id); Assert.AreEqual(Transactions.DefaultTimeoutOnPartitionMapExchange, TimeSpan.Zero); - + DateTime startTime1 = tx.StartTime; tx.Commit(); @@ -863,6 +863,59 @@ namespace Apache.Ignite.Core.Tests.Cache } /// <summary> + /// Tests that read operations lock keys in Serializable mode. + /// </summary> + [Test] + public void TestTransactionScopeWithSerializableIsolationLocksKeysOnRead() + { + Action<Func<ICache<int, int>, int, int>> + test = TestTransactionScopeWithSerializableIsolationLocksKeysOnRead; + + test((cache, key) => cache[key]); + test((cache, key) => cache.Get(key)); + test((cache, key) => cache.GetAsync(key).Result); + test((cache, key) => { int val; return cache.TryGet(key, out val) ? val : 0; }); + test((cache, key) => cache.TryGetAsync(key).Result.Value); + test((cache, key) => cache.GetAll(new[] {key}).Single().Value); + test((cache, key) => cache.GetAllAsync(new[] {key}).Result.Single().Value); + } + + /// <summary> + /// Tests that read operations lock keys in Serializable mode. + /// </summary> + private void TestTransactionScopeWithSerializableIsolationLocksKeysOnRead( + Func<ICache<int, int>, int, int> readOp) + { + var cache = Cache(); + cache.Put(1, 1); + + var options = new TransactionOptions {IsolationLevel = IsolationLevel.Serializable}; + + using (var scope = new TransactionScope(TransactionScopeOption.Required, options)) + { + Assert.AreEqual(1, readOp(cache, 1)); + Assert.IsNotNull(GetIgnite(0).GetTransactions().Tx); + + var evt = new ManualResetEventSlim(); + + var task = Task.Factory.StartNew(() => + { + cache.PutAsync(1, 2); + evt.Set(); + }); + + evt.Wait(); + + Assert.AreEqual(1, readOp(cache, 1)); + + scope.Complete(); + task.Wait(); + } + + TestUtils.WaitForTrueCondition(() => 2 == readOp(cache, 1)); + } + + /// <summary> /// Checks that cache operation behaves transactionally. /// </summary> private void CheckTxOp(Action<ICache<int, int>, int> act) diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/OptimisticTransactionTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/OptimisticTransactionTest.cs new file mode 100644 index 0000000..939b87b --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/OptimisticTransactionTest.cs @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Apache.Ignite.Core.Tests.Cache +{ + using System.Threading.Tasks; + using System.Transactions; + using Apache.Ignite.Core.Cache; + using Apache.Ignite.Core.Cache.Configuration; + using Apache.Ignite.Core.Transactions; + using NUnit.Framework; + + /// <summary> + /// Tests for <see cref="TransactionConcurrency.Optimistic"/> mode. + /// </summary> + public class OptimisticTransactionTest : TestBase + { + /// <summary> + /// Tests explicit optimistic transactions. + /// </summary> + [Test] + public void TestExplicitOptimisticTransactionThrowsOptimisticExceptionOnConflict() + { + var cache = GetCache(); + var transactions = Ignite.GetTransactions(); + + using (var tx = transactions.TxStart()) + { + Assert.IsNotNull(transactions.Tx); + Assert.AreEqual(TransactionConcurrency.Optimistic, tx.Concurrency); + Assert.AreEqual(TransactionIsolation.Serializable, tx.Isolation); + + var old = cache[1]; + + Task.Factory.StartNew(() => + { + Assert.IsNull(transactions.Tx); + cache[1] = -1; + }).Wait(); + + Assert.AreEqual(old, cache[1]); + cache[1] = old + 1; + + var ex = Assert.Throws<TransactionOptimisticException>(() => tx.Commit()); + StringAssert.StartsWith( + "Failed to prepare transaction, read/write conflict [key=1, keyCls=java.lang.Integer, val=-1", + ex.Message); + } + + Assert.AreEqual(-1, cache[1]); + } + + /// <summary> + /// Tests ambient optimistic transactions (with <see cref="TransactionScope"/>). + /// </summary> + [Test] + public void TestAmbientOptimisticTransactionThrowsOptimisticExceptionOnConflict() + { + var cache = GetCache(); + var transactions = Ignite.GetTransactions(); + + var scope = new TransactionScope(); + var old = cache[1]; + + Assert.IsNotNull(transactions.Tx); + Assert.AreEqual(TransactionConcurrency.Optimistic, transactions.Tx.Concurrency); + Assert.AreEqual(TransactionIsolation.Serializable, transactions.Tx.Isolation); + + Task.Factory.StartNew(() => + { + Assert.IsNull(transactions.Tx); + cache[1] = -1; + }).Wait(); + + Assert.AreEqual(old, cache[1]); + cache[1] = old + 1; + + // Complete() just sets a flag, actual Commit is called from Dispose(). + scope.Complete(); + + var ex = Assert.Throws<TransactionOptimisticException>(() => scope.Dispose()); + StringAssert.StartsWith( + "Failed to prepare transaction, read/write conflict [key=1, keyCls=java.lang.Integer, val=-1", + ex.Message); + + Assert.AreEqual(-1, cache[1]); + Assert.IsNull(transactions.Tx); + } + + /** <inheritdoc /> */ + protected override IgniteConfiguration GetConfig() + { + return new IgniteConfiguration(base.GetConfig()) + { + TransactionConfiguration = new TransactionConfiguration + { + DefaultTransactionConcurrency = TransactionConcurrency.Optimistic, + DefaultTransactionIsolation = TransactionIsolation.Serializable + } + }; + } + + /// <summary> + /// Gets the cache. + /// </summary> + private ICache<int, int> GetCache() + { + var cacheConfiguration = new CacheConfiguration(TestUtils.TestName) + { + AtomicityMode = CacheAtomicityMode.Transactional + }; + + var cache = Ignite.GetOrCreateCache<int, int>(cacheConfiguration); + + cache[1] = 1; + + return cache; + } + } +} diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCacheTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCacheTest.cs index 2b79e68..9449312 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCacheTest.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCacheTest.cs @@ -1285,6 +1285,7 @@ namespace Apache.Ignite.Core.Tests.Cache.Platform using (new TransactionScope()) { cache[2] = new Foo(3); + Assert.IsNotNull(_grid.GetTransactions().Tx); Assert.AreNotSame(foo, cache[1]); } diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/CacheImpl.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/CacheImpl.cs index c2b4718..cb96d2e 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/CacheImpl.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Cache/CacheImpl.cs @@ -531,6 +531,8 @@ namespace Apache.Ignite.Core.Impl.Cache { IgniteArgumentCheck.NotNull(key, "key"); + StartTxIfNeeded(); + TV val; if (CanUsePlatformCache && _platformCache.TryGetValue(key, out val)) { @@ -553,6 +555,8 @@ namespace Apache.Ignite.Core.Impl.Cache { IgniteArgumentCheck.NotNull(key, "key"); + StartTxIfNeeded(); + TV val; if (CanUsePlatformCache && _platformCache.TryGetValue(key, out val)) { @@ -573,6 +577,8 @@ namespace Apache.Ignite.Core.Impl.Cache { IgniteArgumentCheck.NotNull(key, "key"); + StartTxIfNeeded(); + if (CanUsePlatformCache && _platformCache.TryGetValue(key, out value)) { return true; @@ -590,6 +596,8 @@ namespace Apache.Ignite.Core.Impl.Cache { IgniteArgumentCheck.NotNull(key, "key"); + StartTxIfNeeded(); + return DoOutOpAsync(CacheOp.GetAsync, w => w.WriteObject(key), reader => GetCacheResult(reader)); } @@ -598,6 +606,8 @@ namespace Apache.Ignite.Core.Impl.Cache { IgniteArgumentCheck.NotNull(keys, "keys"); + StartTxIfNeeded(); + if (CanUsePlatformCache) { // Get what we can from platform cache, and the rest from Java. @@ -660,6 +670,8 @@ namespace Apache.Ignite.Core.Impl.Cache { IgniteArgumentCheck.NotNull(keys, "keys"); + StartTxIfNeeded(); + if (CanUsePlatformCache) { // Get what we can from platform cache, and the rest from Java. diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/ExceptionUtils.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/ExceptionUtils.cs index 12b6de4..8bf956a 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/ExceptionUtils.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/ExceptionUtils.cs @@ -95,6 +95,7 @@ namespace Apache.Ignite.Core.Impl // Transaction exceptions. Exs["org.apache.ignite.transactions.TransactionOptimisticException"] = (c, m, e, i) => new TransactionOptimisticException(m, e); + Exs["org.apache.ignite.internal.transactions.IgniteTxOptimisticCheckedException"] = (c, m, e, i) => new TransactionOptimisticException(m, e); Exs["org.apache.ignite.transactions.TransactionTimeoutException"] = (c, m, e, i) => new TransactionTimeoutException(m, e); Exs["org.apache.ignite.transactions.TransactionRollbackException"] = (c, m, e, i) => new TransactionRollbackException(m, e); Exs["org.apache.ignite.transactions.TransactionHeuristicException"] = (c, m, e, i) => new TransactionHeuristicException(m, e); diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Transactions/CacheTransactionManager.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Transactions/CacheTransactionManager.cs index 9838689..9f92a12 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Transactions/CacheTransactionManager.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Transactions/CacheTransactionManager.cs @@ -61,16 +61,6 @@ namespace Apache.Ignite.Core.Impl.Transactions return; } - if (Enlistment.Value != null) - { - // We are already enlisted. - // .NET transaction mechanism allows nested transactions, - // and they can be processed differently depending on TransactionScopeOption. - // Ignite, however, allows only one active transaction per thread. - // Therefore we enlist only once on the first transaction that we encounter. - return; - } - var ambientTx = System.Transactions.Transaction.Current; if (ambientTx != null && ambientTx.TransactionInformation.Status == TransactionStatus.Active) @@ -82,6 +72,9 @@ namespace Apache.Ignite.Core.Impl.Transactions } } + /// <summary> + /// Gets a value indicating whether there is an active transaction. + /// </summary> public bool IsInTx() { return _transactions.Tx != null; @@ -97,7 +90,16 @@ namespace Apache.Ignite.Core.Impl.Transactions if (igniteTx != null && Enlistment.Value != null) { - ((Transaction) igniteTx).Prepare(); + try + { + ((Transaction) igniteTx).Prepare(); + } + catch (Exception) + { + // Prepare failed - release Ignite transaction (we won't have another chance to do this). + igniteTx.Dispose(); + throw; + } } preparingEnlistment.Prepared();