You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by pt...@apache.org on 2020/07/09 18:42:11 UTC

[ignite] branch ignite-2.9 updated: IGNITE-13214 .NET: Fix TransactionScope for read operations

This is an automated email from the ASF dual-hosted git repository.

ptupitsyn pushed a commit to branch ignite-2.9
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/ignite-2.9 by this push:
     new 00abad7  IGNITE-13214 .NET: Fix TransactionScope for read operations
00abad7 is described below

commit 00abad78c82c67ffc8066e4f8d7327c6e4733561
Author: Pavel Tupitsyn <pt...@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
    
    (cherry picked from commit 0aceb369c0097dadf94fae75456521baf678005a)
---
 .../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();