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();