diff --git a/modules/api/src/main/java/org/apache/ignite/tx/TransactionOptions.java b/modules/api/src/main/java/org/apache/ignite/tx/TransactionOptions.java index 2b785ccd764..96594b5f4fd 100644 --- a/modules/api/src/main/java/org/apache/ignite/tx/TransactionOptions.java +++ b/modules/api/src/main/java/org/apache/ignite/tx/TransactionOptions.java @@ -21,14 +21,14 @@ * Ignite transaction options. */ public class TransactionOptions { - /** Transaction timeout. */ + /** Transaction timeout. 0 means 'use default timeout'. */ private long timeoutMillis = 0; /** Read-only transaction. */ private boolean readOnly = false; /** - * Returns transaction timeout, in milliseconds. + * Returns transaction timeout, in milliseconds. 0 means 'use default timeout'. * * @return Transaction timeout, in milliseconds. */ @@ -36,13 +36,22 @@ public long timeoutMillis() { return timeoutMillis; } + // TODO: remove a note that timeouts are not supported for RW after IGNITE-15936 is implemented. /** * Sets transaction timeout, in milliseconds. * - * @param timeoutMillis Transaction timeout, in milliseconds. + * @param timeoutMillis Transaction timeout, in milliseconds. Cannot be negative; 0 means 'use default timeout'. + * * @return {@code this} for chaining. */ public TransactionOptions timeoutMillis(long timeoutMillis) { + if (timeoutMillis < 0) { + throw new IllegalArgumentException("Negative timeoutMillis: " + timeoutMillis); + } + this.timeoutMillis = timeoutMillis; return this; diff --git a/modules/api/src/test/java/org/apache/ignite/tx/TransactionOptionsTest.java b/modules/api/src/test/java/org/apache/ignite/tx/TransactionOptionsTest.java new file mode 100644 index 00000000000..3f305415329 --- /dev/null +++ b/modules/api/src/test/java/org/apache/ignite/tx/TransactionOptionsTest.java @@ -0,0 +1,91 @@ +/* + * 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. + */ + +package org.apache.ignite.tx; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class TransactionOptionsTest { + @Test + void readOnlyIsFalseByDefault() { + assertThat(new TransactionOptions().readOnly(), is(false)); + } + + @Test + void readOnlyStatusIsSet() { + var options = new TransactionOptions(); + + options.readOnly(true); + + assertThat(options.readOnly(), is(true)); + } + + @Test + void readOnlySetterReturnsSameObject() { + var options = new TransactionOptions(); + + TransactionOptions afterSetting = options.readOnly(true); + + assertSame(options, afterSetting); + } + + @Test + void timeoutIsZeroByDefault() { + assertThat(new TransactionOptions().timeoutMillis(), is(0L)); + } + + @Test + void timeoutIsSet() { + var options = new TransactionOptions(); + + options.timeoutMillis(3333); + + assertThat(options.timeoutMillis(), is(3333L)); + } + + @Test + void timeoutSetterReturnsSameObject() { + var options = new TransactionOptions(); + + TransactionOptions afterSetting = options.timeoutMillis(3333); + + assertSame(options, afterSetting); + } + + @Test + void positiveTimeoutIsAllowed() { + assertDoesNotThrow(() -> new TransactionOptions().timeoutMillis(0)); + } + + @Test + void zeroTimeoutIsAllowed() { + assertDoesNotThrow(() -> new TransactionOptions().timeoutMillis(0)); + } + + @Test + void negativeTimeoutIsRejected() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new TransactionOptions().timeoutMillis(-1)); + + assertThat(ex.getMessage(), is("Negative timeoutMillis: -1")); + } +} diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java index 18ec268eb94..6b3c71c5123 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java @@ -27,7 +27,6 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.handler.ssl.SslContext; @@ -130,7 +129,7 @@ public class ClientHandlerModule implements IgniteComponent { @TestOnly @SuppressWarnings("unused") - private volatile ChannelHandler handler; + private volatile ClientInboundMessageHandler handler; /** * Constructor. @@ -396,4 +395,9 @@ private ClientInboundMessageHandler createInboundMessageHandler(ClientConnectorV partitionOperationsExecutor ); } + + @TestOnly + public ClientInboundMessageHandler handler() { + return handler; + } } diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java index c5dc979615f..c7e83e5580b 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java @@ -139,6 +139,7 @@ import org.apache.ignite.security.exception.UnsupportedAuthenticationTypeException; import org.apache.ignite.sql.SqlBatchException; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; /** * Handles messages from thin clients. @@ -1139,4 +1140,9 @@ private static Set authenticationEventsToSubscribe() { AuthenticationEvent.USER_REMOVED ); } + + @TestOnly + public ClientResourceRegistry resources() { + return resources; + } } diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java index 3662ce2abce..4614a044236 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java @@ -67,6 +67,7 @@ import org.apache.ignite.internal.sql.engine.property.SqlPropertiesHelper; import org.apache.ignite.internal.tx.HybridTimestampTracker; import org.apache.ignite.internal.tx.InternalTransaction; +import org.apache.ignite.internal.tx.InternalTxOptions; import org.apache.ignite.internal.tx.TxManager; import org.apache.ignite.internal.util.AsyncCursor.BatchedResult; import org.apache.ignite.lang.CancelHandle; @@ -186,8 +187,8 @@ public HybridTimestampTracker getTimestampTracker() { } private static SqlProperties createProperties( - JdbcStatementType stmtType, - boolean multiStatement, + JdbcStatementType stmtType, + boolean multiStatement, ZoneId timeZoneId, long queryTimeoutMillis ) { @@ -452,7 +453,7 @@ ZoneId timeZoneId() { * @return Transaction associated with the current connection. */ InternalTransaction getOrStartTransaction(HybridTimestampTracker timestampProvider) { - return tx == null ? tx = txManager.begin(timestampProvider, false) : tx; + return tx == null ? tx = txManager.beginExplicitRw(timestampProvider, InternalTxOptions.defaults()) : tx; } /** diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java index fdc1aa3b71c..e1d156a7d2d 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java @@ -44,6 +44,7 @@ import org.apache.ignite.internal.table.TableViewInternal; import org.apache.ignite.internal.tx.HybridTimestampTracker; import org.apache.ignite.internal.tx.InternalTransaction; +import org.apache.ignite.internal.tx.InternalTxOptions; import org.apache.ignite.internal.tx.TxManager; import org.apache.ignite.internal.type.DecimalNativeType; import org.apache.ignite.internal.type.NativeType; @@ -439,32 +440,53 @@ public static TableNotFoundException tableIdNotFoundException(Integer tableId) { if (tx == null) { // Implicit transactions do not use an observation timestamp because RW never depends on it, and implicit RO is always direct. // The direct transaction uses a current timestamp on the primary replica by definition. - tx = startTx(out, txManager, null, true, readOnly); + tx = startImplicitTx(out, txManager, null, readOnly); } return tx; } /** - * Start a transaction. + * Starts an explicit transaction. * * @param out Packer. * @param txManager Ignite transactions. * @param currentTs Current observation timestamp or {@code null} if it is not defined. - * @param implicit Implicit transaction flag. * @param readOnly Read only flag. + * @param options Transaction options. * @return Transaction. */ - public static InternalTransaction startTx( + public static InternalTransaction startExplicitTx( + ClientMessagePacker out, + TxManager txManager, + @Nullable HybridTimestamp currentTs, + boolean readOnly, + InternalTxOptions options + ) { + return txManager.beginExplicit( + HybridTimestampTracker.clientTracker(currentTs, ts -> {}), + readOnly, + options + ); + } + + /** + * Starts an implicit transaction. + * + * @param out Packer. + * @param txManager Ignite transactions. + * @param currentTs Current observation timestamp or {@code null} if it is not defined. + * @param readOnly Read only flag. + * @return Transaction. + */ + public static InternalTransaction startImplicitTx( ClientMessagePacker out, TxManager txManager, @Nullable HybridTimestamp currentTs, - boolean implicit, boolean readOnly ) { - return txManager.begin( - HybridTimestampTracker.clientTracker(currentTs, implicit ? out::meta : ts -> {}), - implicit, + return txManager.beginImplicit( + HybridTimestampTracker.clientTracker(currentTs, out::meta), readOnly ); } diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/tx/ClientTransactionBeginRequest.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/tx/ClientTransactionBeginRequest.java index 5237f6bf91e..6f804045b05 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/tx/ClientTransactionBeginRequest.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/tx/ClientTransactionBeginRequest.java @@ -17,7 +17,7 @@ package org.apache.ignite.client.handler.requests.tx; -import static org.apache.ignite.client.handler.requests.table.ClientTableCommon.startTx; +import static org.apache.ignite.client.handler.requests.table.ClientTableCommon.startExplicitTx; import java.util.concurrent.CompletableFuture; import org.apache.ignite.client.handler.ClientHandlerMetricSource; @@ -27,6 +27,7 @@ import org.apache.ignite.internal.client.proto.ClientMessageUnpacker; import org.apache.ignite.internal.hlc.HybridTimestamp; import org.apache.ignite.internal.lang.IgniteInternalCheckedException; +import org.apache.ignite.internal.tx.InternalTxOptions; import org.apache.ignite.internal.tx.TxManager; import org.jetbrains.annotations.Nullable; @@ -52,12 +53,20 @@ public class ClientTransactionBeginRequest { ClientHandlerMetricSource metrics ) throws IgniteInternalCheckedException { boolean readOnly = in.unpackBoolean(); + long timeoutMillis = in.unpackLong(); - // Timestamp makes sense only for read-only transactions. - HybridTimestamp observableTs = readOnly ? HybridTimestamp.nullableHybridTimestamp(in.unpackLong()) : null; + HybridTimestamp observableTs = null; + if (readOnly) { + // Timestamp makes sense only for read-only transactions. + observableTs = HybridTimestamp.nullableHybridTimestamp(in.unpackLong()); + } + + InternalTxOptions txOptions = InternalTxOptions.builder() + .timeoutMillis(timeoutMillis) + .build(); // NOTE: we don't use beginAsync here because it is synchronous anyway. - var tx = startTx(out, txManager, observableTs, false, readOnly); + var tx = startExplicitTx(out, txManager, observableTs, readOnly, txOptions); if (readOnly) { // For read-only tx, override observable timestamp that we send to the client: diff --git a/modules/client-handler/src/test/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImplTest.java b/modules/client-handler/src/test/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImplTest.java index 69f7e6df60a..4df09296b48 100644 --- a/modules/client-handler/src/test/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImplTest.java +++ b/modules/client-handler/src/test/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImplTest.java @@ -29,7 +29,6 @@ import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -133,7 +132,7 @@ public void contextClosedDuringBatchQuery() throws Exception { CountDownLatch registryCloseLatch = new CountDownLatch(1); long connectionId = acquireConnectionId(); - when(txManager.begin(any(), eq(false))).thenAnswer(v -> { + when(txManager.beginExplicitRw(any(), any())).thenAnswer(v -> { registryCloseLatch.countDown(); assertThat(startTxLatch.await(timeout, TimeUnit.SECONDS), is(true)); @@ -161,13 +160,13 @@ public void explicitTxRollbackOnCloseRegistry() { InternalTransaction tx = mock(InternalTransaction.class); when(tx.rollbackAsync()).thenReturn(nullCompletedFuture()); - when(txManager.begin(any(), eq(false))).thenReturn(tx); + when(txManager.beginExplicitRw(any(), any())).thenReturn(tx); long connectionId = acquireConnectionId(); await(eventHandler.batchAsync(connectionId, createExecuteBatchRequest("x", "UPDATE 1"))); - verify(txManager).begin(any(), eq(false)); + verify(txManager).beginExplicitRw(any(), any()); verify(tx, times(0)).rollbackAsync(); resourceRegistry.close(); @@ -183,10 +182,10 @@ public void singleTxUsedForMultipleOperations() { InternalTransaction tx = mock(InternalTransaction.class); when(tx.commitAsync()).thenReturn(nullCompletedFuture()); when(tx.rollbackAsync()).thenReturn(nullCompletedFuture()); - when(txManager.begin(any(), eq(false))).thenReturn(tx); + when(txManager.beginExplicitRw(any(), any())).thenReturn(tx); long connectionId = acquireConnectionId(); - verify(txManager, times(0)).begin(any(), eq(false)); + verify(txManager, times(0)).beginExplicitRw(any(), any()); String schema = "schema"; JdbcStatementType type = JdbcStatementType.SELECT_STATEMENT_TYPE; @@ -194,21 +193,21 @@ public void singleTxUsedForMultipleOperations() { await(eventHandler.queryAsync( connectionId, createExecuteRequest(schema, "SELECT 1", type) )); - verify(txManager, times(1)).begin(any(), eq(false)); + verify(txManager, times(1)).beginExplicitRw(any(), any()); await(eventHandler.batchAsync(connectionId, createExecuteBatchRequest("schema", "UPDATE 1", "UPDATE 2"))); - verify(txManager, times(1)).begin(any(), eq(false)); + verify(txManager, times(1)).beginExplicitRw(any(), any()); await(eventHandler.finishTxAsync(connectionId, false)); verify(tx).rollbackAsync(); await(eventHandler.batchAsync(connectionId, createExecuteBatchRequest("schema", "UPDATE 1", "UPDATE 2"))); - verify(txManager, times(2)).begin(any(), eq(false)); + verify(txManager, times(2)).beginExplicitRw(any(), any()); await(eventHandler.queryAsync( connectionId, createExecuteRequest(schema, "SELECT 2", type) )); - verify(txManager, times(2)).begin(any(), eq(false)); + verify(txManager, times(2)).beginExplicitRw(any(), any()); await(eventHandler.batchAsync(connectionId, createExecuteBatchRequest("schema", "UPDATE 3", "UPDATE 4"))); - verify(txManager, times(2)).begin(any(), eq(false)); + verify(txManager, times(2)).beginExplicitRw(any(), any()); await(eventHandler.finishTxAsync(connectionId, true)); verify(tx).commitAsync(); @@ -223,7 +222,7 @@ void simpleQueryCancellation() { long connectionId = acquireConnectionId(); - JdbcQueryExecuteRequest executeRequest = createExecuteRequest("schema", "SELECT 1", JdbcStatementType.SELECT_STATEMENT_TYPE); + JdbcQueryExecuteRequest executeRequest = createExecuteRequest("schema", "SELECT 1", JdbcStatementType.SELECT_STATEMENT_TYPE); CompletableFuture resultFuture = eventHandler.queryAsync(connectionId, executeRequest); diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientLazyTransaction.java b/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientLazyTransaction.java index 133ec00f7b2..6e727999d41 100644 --- a/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientLazyTransaction.java +++ b/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientLazyTransaction.java @@ -165,7 +165,10 @@ private synchronized CompletableFuture ensureStarted( return tx0; } - ClientTransaction startedTx() { + /** + * Returns actual {@link ClientTransaction} started by this transaction. + */ + public ClientTransaction startedTx() { var tx0 = tx; assert tx0 != null : "Transaction is not started"; diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransactions.java b/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransactions.java index 325ed680a7c..ece4c13b584 100644 --- a/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransactions.java +++ b/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransactions.java @@ -62,9 +62,9 @@ static CompletableFuture beginAsync( @Nullable String preferredNodeName, @Nullable TransactionOptions options, long observableTimestamp) { - if (options != null && options.timeoutMillis() != 0) { + if (options != null && options.timeoutMillis() != 0 && !options.readOnly()) { // TODO: IGNITE-16193 - throw new UnsupportedOperationException("Timeouts are not supported yet"); + throw new UnsupportedOperationException("Timeouts are not supported yet for RW transactions"); } boolean readOnly = options != null && options.readOnly(); @@ -73,6 +73,7 @@ static CompletableFuture beginAsync( ClientOp.TX_BEGIN, w -> { w.out().packBoolean(readOnly); + w.out().packLong(options == null ? 0 : options.timeoutMillis()); w.out().packLong(observableTimestamp); }, r -> readTx(r, readOnly), diff --git a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeTxManager.java b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeTxManager.java index eb5dbeb74e7..8cefbecb722 100644 --- a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeTxManager.java +++ b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeTxManager.java @@ -31,9 +31,9 @@ import org.apache.ignite.internal.replicator.TablePartitionId; import org.apache.ignite.internal.tx.HybridTimestampTracker; import org.apache.ignite.internal.tx.InternalTransaction; +import org.apache.ignite.internal.tx.InternalTxOptions; import org.apache.ignite.internal.tx.LockManager; import org.apache.ignite.internal.tx.TxManager; -import org.apache.ignite.internal.tx.TxPriority; import org.apache.ignite.internal.tx.TxState; import org.apache.ignite.internal.tx.TxStateMeta; import org.apache.ignite.network.ClusterNode; @@ -63,17 +63,16 @@ public CompletableFuture stopAsync(ComponentContext componentContext) { } @Override - public InternalTransaction begin(HybridTimestampTracker tracker, boolean implicit) { - return begin(tracker, implicit, false); + public InternalTransaction beginImplicit(HybridTimestampTracker timestampTracker, boolean readOnly) { + return begin(timestampTracker, true, readOnly, InternalTxOptions.defaults()); } @Override - public InternalTransaction begin(HybridTimestampTracker timestampTracker, boolean implicit, boolean readOnly) { - return begin(timestampTracker, implicit, readOnly, TxPriority.NORMAL); + public InternalTransaction beginExplicit(HybridTimestampTracker timestampTracker, boolean readOnly, InternalTxOptions txOptions) { + return begin(timestampTracker, false, readOnly, txOptions); } - @Override - public InternalTransaction begin(HybridTimestampTracker tracker, boolean implicit, boolean readOnly, TxPriority priority) { + private InternalTransaction begin(HybridTimestampTracker tracker, boolean implicit, boolean readOnly, InternalTxOptions options) { return new InternalTransaction() { private final UUID id = UUID.randomUUID(); diff --git a/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/rebalance/ItRebalanceDistributedTest.java b/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/rebalance/ItRebalanceDistributedTest.java index 76bb0b0c87d..383308447c4 100644 --- a/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/rebalance/ItRebalanceDistributedTest.java +++ b/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/rebalance/ItRebalanceDistributedTest.java @@ -331,7 +331,7 @@ public class ItRebalanceDistributedTest extends BaseIgniteAbstractTest { private Path workDir; @InjectExecutorService - private static ScheduledExecutorService commonScheduledExecutorService; + private ScheduledExecutorService commonScheduledExecutorService; private StaticNodeFinder finder; @@ -1411,7 +1411,8 @@ private class Node { new TestLocalRwTxCounter(), resourcesRegistry, transactionInflights, - lowWatermark + lowWatermark, + commonScheduledExecutorService ); rebalanceScheduler = new ScheduledThreadPoolExecutor(REBALANCE_SCHEDULER_POOL_SIZE, diff --git a/modules/partition-replicator/src/integrationTest/java/org/apache/ignite/internal/partition/replicator/ItReplicaLifecycleTest.java b/modules/partition-replicator/src/integrationTest/java/org/apache/ignite/internal/partition/replicator/ItReplicaLifecycleTest.java index 6280a5e0f6c..972dd217c59 100644 --- a/modules/partition-replicator/src/integrationTest/java/org/apache/ignite/internal/partition/replicator/ItReplicaLifecycleTest.java +++ b/modules/partition-replicator/src/integrationTest/java/org/apache/ignite/internal/partition/replicator/ItReplicaLifecycleTest.java @@ -1237,7 +1237,8 @@ public CompletableFuture invoke( new TestLocalRwTxCounter(), resourcesRegistry, transactionInflights, - lowWatermark + lowWatermark, + threadPoolsManager.commonScheduler() ); replicaManager = new ReplicaManager( diff --git a/modules/platforms/cpp/ignite/client/detail/transaction/transactions_impl.h b/modules/platforms/cpp/ignite/client/detail/transaction/transactions_impl.h index cbaedbb0597..7c00c610a5c 100644 --- a/modules/platforms/cpp/ignite/client/detail/transaction/transactions_impl.h +++ b/modules/platforms/cpp/ignite/client/detail/transaction/transactions_impl.h @@ -57,6 +57,7 @@ class transactions_impl { IGNITE_API void begin_async(ignite_callback callback) { auto writer_func = [this](protocol::writer &writer) { writer.write_bool(false); // readOnly. + writer.write(std::int64_t(0)); // timeoutMillis. writer.write(m_connection->get_observable_timestamp()); }; diff --git a/modules/platforms/cpp/ignite/odbc/sql_connection.cpp b/modules/platforms/cpp/ignite/odbc/sql_connection.cpp index d043b3b536a..ea707d48799 100644 --- a/modules/platforms/cpp/ignite/odbc/sql_connection.cpp +++ b/modules/platforms/cpp/ignite/odbc/sql_connection.cpp @@ -433,6 +433,7 @@ void sql_connection::transaction_start() { network::data_buffer_owning response = sync_request(protocol::client_operation::TX_BEGIN, [&](protocol::writer &writer) { writer.write_bool(false); // read_only. + writer.write(std::int64_t(0)); // timeoutMillis. }); protocol::reader reader(response.get_bytes_view()); diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs index c0bdd678ac2..550e178434a 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs @@ -309,6 +309,7 @@ protected override void Handle(Socket handler, CancellationToken cancellationTok case ClientOp.TxBegin: reader.Skip(); // Read only. + reader.Skip(); // TimeoutMillis. LastClientObservableTimestamp = reader.ReadInt64(); Send(handler, requestId, new byte[] { 0 }.AsMemory()); diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Transactions/LazyTransaction.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Transactions/LazyTransaction.cs index c2976cf5fa5..933ad33b336 100644 --- a/modules/platforms/dotnet/Apache.Ignite/Internal/Transactions/LazyTransaction.cs +++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Transactions/LazyTransaction.cs @@ -197,6 +197,7 @@ void Write() { var w = writer.MessageWriter; w.Write(_options.ReadOnly); + w.Write(_options.TimeoutMillis); w.Write(failoverSocket.ObservableTimestamp); } } diff --git a/modules/platforms/dotnet/Apache.Ignite/Transactions/TransactionOptions.cs b/modules/platforms/dotnet/Apache.Ignite/Transactions/TransactionOptions.cs index acc005bdccc..8a94718c2a4 100644 --- a/modules/platforms/dotnet/Apache.Ignite/Transactions/TransactionOptions.cs +++ b/modules/platforms/dotnet/Apache.Ignite/Transactions/TransactionOptions.cs @@ -25,4 +25,10 @@ namespace Apache.Ignite.Transactions; /// Read-only transactions provide a snapshot view of data at a certain point in time. /// They are lock-free and perform better than normal transactions, but do not permit data modifications. /// -public readonly record struct TransactionOptions(bool ReadOnly); +/// +/// Transaction timeout. 0 means 'use default timeout'. +/// For RO transactions, the default timeout is data availability time configured via ignite.gc.lowWatermark.dataAvailabilityTime +/// configuration setting. +/// For RW transactions, timeouts are not supported yet. TODO: IGNITE-15936. +/// +public readonly record struct TransactionOptions(bool ReadOnly, long TimeoutMillis = 0); diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteNodeRestartTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteNodeRestartTest.java index 90e67ba95e7..fee0635e903 100644 --- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteNodeRestartTest.java +++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteNodeRestartTest.java @@ -631,7 +631,8 @@ public CompletableFuture invoke(Condition condition, List su threadPoolsManager.partitionOperationsExecutor(), resourcesRegistry, transactionInflights, - lowWatermark + lowWatermark, + threadPoolsManager.commonScheduler() ); ResourceVacuumManager resourceVacuumManager = new ResourceVacuumManager( diff --git a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java index a2015120784..2c86070a636 100644 --- a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java +++ b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java @@ -60,6 +60,7 @@ import org.apache.ignite.catalog.IgniteCatalog; import org.apache.ignite.client.handler.ClientHandlerMetricSource; import org.apache.ignite.client.handler.ClientHandlerModule; +import org.apache.ignite.client.handler.ClientInboundMessageHandler; import org.apache.ignite.client.handler.ClusterInfo; import org.apache.ignite.client.handler.configuration.ClientConnectorConfiguration; import org.apache.ignite.client.handler.configuration.ClientConnectorExtensionConfiguration; @@ -358,7 +359,7 @@ public class IgniteImpl implements Ignite { private final ReplicaManager replicaMgr; /** Transactions manager. */ - private final TxManager txManager; + private final TxManagerImpl txManager; /** Distributed table manager. */ private final TableManager distributedTblMgr; @@ -989,10 +990,11 @@ public class IgniteImpl implements Ignite { threadPoolsManager.partitionOperationsExecutor(), resourcesRegistry, transactionInflights, - lowWatermark + lowWatermark, + threadPoolsManager.commonScheduler() ); - systemViewManager.register((TxManagerImpl) txManager); + systemViewManager.register(txManager); resourceVacuumManager = new ResourceVacuumManager( name, @@ -1878,17 +1880,17 @@ public LogStorageFactory partitionsLogStorageFactory() { return partitionsLogStorageFactory; } - @TestOnly - public LogStorageFactory volatileLogStorageFactory() { - return volatileLogStorageFactoryCreator.factory(raftMgr.volatileRaft().logStorageBudget().value()); - } - /** Returns the node's transaction manager. */ @TestOnly public TxManager txManager() { return txManager; } + @TestOnly + public ClientInboundMessageHandler clientInboundMessageHandler() { + return clientHandlerModule.handler(); + } + /** Returns the node's placement driver service. */ @TestOnly public PlacementDriver placementDriver() { diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/sqllogic/ItSqlLogicTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/sqllogic/ItSqlLogicTest.java index bee186f5e3e..49df7bca717 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/sqllogic/ItSqlLogicTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/sqllogic/ItSqlLogicTest.java @@ -351,7 +351,8 @@ private static void startNodes() { .clusterName("cluster") .clusterConfiguration("ignite {" + "metaStorage.idleSyncTimeInterval: " + METASTORAGE_IDLE_SYNC_TIME_INTERVAL_MS + ",\n" - + "gc.lowWatermark.dataAvailabilityTime: 5000,\n" + // TODO: Set dataAvailabilityTime to 5000 after IGNITE-24002 is fixed. + + "gc.lowWatermark.dataAvailabilityTime: 30000,\n" + "gc.lowWatermark.updateInterval: 1000,\n" + "metrics.exporters.logPush.exporterName: logPush,\n" + "metrics.exporters.logPush.period: 5000\n" diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/sqllogic/ScriptContext.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/sqllogic/ScriptContext.java index a8815808c68..641a83b4397 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/sqllogic/ScriptContext.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/sqllogic/ScriptContext.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.apache.ignite.internal.logger.IgniteLogger; import org.apache.ignite.internal.sql.sqllogic.SqlScriptRunner.RunnerRuntime; import org.apache.ignite.sql.IgniteSql; @@ -78,7 +79,9 @@ final class ScriptContext { List> executeQuery(String sql) { sql = replaceVars(sql); - log.info("Execute: " + sql); + log.info("Execute: {}", sql); + + long startNanos = System.nanoTime(); try (ResultSet rs = ignSql.execute(null, sql)) { if (rs.hasRowSet()) { @@ -100,6 +103,9 @@ List> executeQuery(String sql) { } else { return Collections.singletonList(Collections.singletonList(rs.wasApplied())); } + } finally { + long tookNanos = System.nanoTime() - startNanos; + log.info("Execution took {} ms", TimeUnit.NANOSECONDS.toMillis(tookNanos)); } } diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/tx/QueryTransactionContextImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/tx/QueryTransactionContextImpl.java index f1db775f409..ee5275787ee 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/tx/QueryTransactionContextImpl.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/tx/QueryTransactionContextImpl.java @@ -24,6 +24,7 @@ import org.apache.ignite.internal.sql.engine.exec.TransactionTracker; import org.apache.ignite.internal.tx.HybridTimestampTracker; import org.apache.ignite.internal.tx.InternalTransaction; +import org.apache.ignite.internal.tx.InternalTxOptions; import org.apache.ignite.internal.tx.TxManager; import org.apache.ignite.tx.TransactionException; import org.jetbrains.annotations.Nullable; @@ -58,7 +59,12 @@ public QueryTransactionWrapper getOrStartSqlManaged(boolean readOnly, boolean im QueryTransactionWrapper result; if (tx == null) { - transaction = txManager.begin(observableTimeTracker, implicit, readOnly); + if (implicit) { + transaction = txManager.beginImplicit(observableTimeTracker, readOnly); + } else { + transaction = txManager.beginExplicit(observableTimeTracker, readOnly, InternalTxOptions.defaults()); + } + result = new QueryTransactionWrapperImpl(transaction, true, txTracker); } else { transaction = tx.unwrap(); diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/QueryTransactionWrapperSelfTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/QueryTransactionWrapperSelfTest.java index 6cc20636a42..394241d9d74 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/QueryTransactionWrapperSelfTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/QueryTransactionWrapperSelfTest.java @@ -131,7 +131,7 @@ public void throwsExceptionForNestedScriptTransaction() { ); IgniteSqlStartTransaction txStartStmt = mock(IgniteSqlStartTransaction.class); - when(txManager.begin(any(), anyBoolean(), anyBoolean())).thenAnswer(inv -> { + when(txManager.beginExplicit(any(), anyBoolean(), any())).thenAnswer(inv -> { boolean implicit = inv.getArgument(1, Boolean.class); return NoOpTransaction.readWrite("test", implicit); @@ -222,12 +222,11 @@ public void testScriptTransactionWrapperTxInflightsInteraction() { } private void prepareTransactionsMocks() { - when(txManager.begin(any(), anyBoolean(), anyBoolean())).thenAnswer( + when(txManager.beginExplicit(any(), anyBoolean(), any())).thenAnswer( inv -> { - boolean implicit = inv.getArgument(1, Boolean.class); - boolean readOnly = inv.getArgument(2, Boolean.class); + boolean readOnly = inv.getArgument(1, Boolean.class); - return readOnly ? NoOpTransaction.readOnly("test-ro", implicit) : NoOpTransaction.readWrite("test-rw", implicit); + return readOnly ? NoOpTransaction.readOnly("test-ro", false) : NoOpTransaction.readWrite("test-rw", false); } ); } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/TableScanNodeExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/TableScanNodeExecutionTest.java index d98e0cbbf14..c7b780f271b 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/TableScanNodeExecutionTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/TableScanNodeExecutionTest.java @@ -36,6 +36,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.Flow.Publisher; import java.util.concurrent.Flow.Subscription; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -75,6 +76,8 @@ import org.apache.ignite.internal.storage.engine.MvTableStorage; import org.apache.ignite.internal.table.StreamerReceiverRunner; import org.apache.ignite.internal.table.distributed.storage.InternalTableImpl; +import org.apache.ignite.internal.testframework.ExecutorServiceExtension; +import org.apache.ignite.internal.testframework.InjectExecutorService; import org.apache.ignite.internal.tx.HybridTimestampTracker; import org.apache.ignite.internal.tx.TxManager; import org.apache.ignite.internal.tx.configuration.TransactionConfiguration; @@ -97,12 +100,16 @@ * Tests execution flow of TableScanNode. */ @ExtendWith(ConfigurationExtension.class) +@ExtendWith(ExecutorServiceExtension.class) public class TableScanNodeExecutionTest extends AbstractExecutionTest { private final LinkedList closeables = new LinkedList<>(); @InjectConfiguration private TransactionConfiguration txConfiguration; + @InjectExecutorService + private ScheduledExecutorService commonExecutor; + // Ensures that all data from TableScanNode is being propagated correctly. @Test public void testScanNodeDataPropagation() throws InterruptedException { @@ -171,7 +178,8 @@ public void testScanNodeDataPropagation() throws InterruptedException { new TestLocalRwTxCounter(), resourcesRegistry, transactionInflights, - new TestLowWatermark() + new TestLowWatermark(), + commonExecutor ); assertThat(txManager.startAsync(new ComponentContext()), willCompleteSuccessfully()); diff --git a/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItInternalTableReadOnlyScanTest.java b/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItInternalTableReadOnlyScanTest.java index 14024a33c7f..1e33ce0e110 100644 --- a/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItInternalTableReadOnlyScanTest.java +++ b/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItInternalTableReadOnlyScanTest.java @@ -26,6 +26,7 @@ import org.apache.ignite.internal.table.InternalTable; import org.apache.ignite.internal.tx.HybridTimestampTracker; import org.apache.ignite.internal.tx.InternalTransaction; +import org.apache.ignite.internal.tx.InternalTxOptions; import org.apache.ignite.network.ClusterNode; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; @@ -48,7 +49,7 @@ protected Publisher scan(int part, @Nullable InternalTransaction tx) @Override protected InternalTransaction startTx() { - return internalTbl.txManager().begin(HYBRID_TIMESTAMP_TRACKER, false, true); + return internalTbl.txManager().beginExplicitRo(HYBRID_TIMESTAMP_TRACKER, InternalTxOptions.defaults()); } @Override diff --git a/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItInternalTableReadWriteScanTest.java b/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItInternalTableReadWriteScanTest.java index 7f1c58925c4..13c3c221603 100644 --- a/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItInternalTableReadWriteScanTest.java +++ b/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItInternalTableReadWriteScanTest.java @@ -27,6 +27,7 @@ import org.apache.ignite.internal.table.RollbackTxOnErrorPublisher; import org.apache.ignite.internal.tx.HybridTimestampTracker; import org.apache.ignite.internal.tx.InternalTransaction; +import org.apache.ignite.internal.tx.InternalTxOptions; import org.apache.ignite.internal.utils.PrimaryReplica; import org.apache.ignite.network.ClusterNode; import org.jetbrains.annotations.Nullable; @@ -71,7 +72,7 @@ public void testInvalidPartitionParameterScan() { @Override protected InternalTransaction startTx() { - InternalTransaction tx = internalTbl.txManager().begin(HYBRID_TIMESTAMP_TRACKER, false); + InternalTransaction tx = internalTbl.txManager().beginExplicitRw(HYBRID_TIMESTAMP_TRACKER, InternalTxOptions.defaults()); TablePartitionId tblPartId = new TablePartitionId(internalTbl.tableId(), ((TablePartitionId) internalTbl.groupId()).partitionId()); diff --git a/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItLockTableTest.java b/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItLockTableTest.java index 25c5b62ad94..f2c2331c7fb 100644 --- a/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItLockTableTest.java +++ b/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItLockTableTest.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; import org.apache.ignite.internal.configuration.SystemLocalConfiguration; import org.apache.ignite.internal.configuration.testframework.ConfigurationExtension; import org.apache.ignite.internal.configuration.testframework.InjectConfiguration; @@ -39,7 +40,9 @@ import org.apache.ignite.internal.schema.SchemaDescriptor; import org.apache.ignite.internal.schema.configuration.StorageUpdateConfiguration; import org.apache.ignite.internal.table.TableViewInternal; +import org.apache.ignite.internal.testframework.ExecutorServiceExtension; import org.apache.ignite.internal.testframework.IgniteAbstractTest; +import org.apache.ignite.internal.testframework.InjectExecutorService; import org.apache.ignite.internal.tx.HybridTimestampTracker; import org.apache.ignite.internal.tx.configuration.TransactionConfiguration; import org.apache.ignite.internal.tx.impl.HeapLockManager; @@ -65,6 +68,7 @@ * Test lock table. */ @ExtendWith(ConfigurationExtension.class) +@ExtendWith(ExecutorServiceExtension.class) public class ItLockTableTest extends IgniteAbstractTest { private static final IgniteLogger LOG = Loggers.forClass(ItLockTableTest.class); @@ -101,6 +105,9 @@ public class ItLockTableTest extends IgniteAbstractTest { @InjectConfiguration("mock.properties: { lockMapSize: \"" + CACHE_SIZE + "\", rawSlotsMaxSize: \"131072\" }") private static SystemLocalConfiguration systemLocalConfiguration; + @InjectExecutorService + protected ScheduledExecutorService commonExecutor; + private ItTxTestCluster txTestCluster; private HybridTimestampTracker timestampTracker = HybridTimestampTracker.atomicTracker(null); @@ -152,7 +159,8 @@ protected TxManagerImpl newTxManager( new TestLocalRwTxCounter(), resourcesRegistry, transactionInflights, - lowWatermark + lowWatermark, + commonExecutor ); } }; diff --git a/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItTxDistributedTestSingleNodeNoCleanupMessage.java b/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItTxDistributedTestSingleNodeNoCleanupMessage.java index 4fcec7dec76..08d71c75267 100644 --- a/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItTxDistributedTestSingleNodeNoCleanupMessage.java +++ b/modules/table/src/integrationTest/java/org/apache/ignite/distributed/ItTxDistributedTestSingleNodeNoCleanupMessage.java @@ -139,7 +139,8 @@ protected TxManagerImpl newTxManager( new TestLocalRwTxCounter(), resourcesRegistry, transactionInflights, - lowWatermark + lowWatermark, + commonExecutor ) { @Override public CompletableFuture executeWriteIntentSwitchAsync(Runnable runnable) { diff --git a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItColocationTest.java b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItColocationTest.java index 162cbebf507..f6fcefd9e79 100644 --- a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItColocationTest.java +++ b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItColocationTest.java @@ -49,6 +49,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -100,6 +101,8 @@ import org.apache.ignite.internal.table.impl.DummyInternalTableImpl; import org.apache.ignite.internal.table.impl.DummySchemaManagerImpl; import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest; +import org.apache.ignite.internal.testframework.ExecutorServiceExtension; +import org.apache.ignite.internal.testframework.InjectExecutorService; import org.apache.ignite.internal.tx.HybridTimestampTracker; import org.apache.ignite.internal.tx.LockManager; import org.apache.ignite.internal.tx.TxManager; @@ -133,6 +136,7 @@ * Tests for data colocation. */ @ExtendWith(ConfigurationExtension.class) +@ExtendWith(ExecutorServiceExtension.class) public class ItColocationTest extends BaseIgniteAbstractTest { /** Partitions count. */ private static final int PARTS = 32; @@ -163,6 +167,9 @@ public class ItColocationTest extends BaseIgniteAbstractTest { @InjectConfiguration private static SystemLocalConfiguration systemLocalConfiguration; + @InjectExecutorService + private static ScheduledExecutorService commonExecutor; + private SchemaDescriptor schema; private SchemaRegistry schemaRegistry; @@ -202,7 +209,8 @@ static void beforeAllTests() { new TestLocalRwTxCounter(), resourcesRegistry, transactionInflights, - new TestLowWatermark() + new TestLowWatermark(), + commonExecutor ) { @Override public CompletableFuture finish( diff --git a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/storage/InternalTableImpl.java b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/storage/InternalTableImpl.java index eaf214ee076..1f1a61d946f 100644 --- a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/storage/InternalTableImpl.java +++ b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/storage/InternalTableImpl.java @@ -501,11 +501,11 @@ private CompletableFuture enlistInTx( } private InternalTransaction startImplicitRwTxIfNeeded(@Nullable InternalTransaction tx) { - return tx == null ? txManager.begin(observableTimestampTracker, true) : tx; + return tx == null ? txManager.beginImplicitRw(observableTimestampTracker) : tx; } private InternalTransaction startImplicitRoTxIfNeeded(@Nullable InternalTransaction tx) { - return tx == null ? txManager.begin(observableTimestampTracker, true, true) : tx; + return tx == null ? txManager.beginImplicitRo(observableTimestampTracker) : tx; } /** @@ -1169,7 +1169,7 @@ private CompletableFuture updateAllWithRetry( int partition, @Nullable Long txStartTs ) { - InternalTransaction tx = txManager.begin(observableTimestampTracker, true); + InternalTransaction tx = txManager.beginImplicitRw(observableTimestampTracker); TablePartitionId partGroupId = new TablePartitionId(tableId, partition); assert rows.stream().allMatch(row -> partitionId(row) == partition) : "Invalid batch for partition " + partition; diff --git a/modules/table/src/testFixtures/java/org/apache/ignite/distributed/ItTxTestCluster.java b/modules/table/src/testFixtures/java/org/apache/ignite/distributed/ItTxTestCluster.java index 1c052875c7f..49ab15b1577 100644 --- a/modules/table/src/testFixtures/java/org/apache/ignite/distributed/ItTxTestCluster.java +++ b/modules/table/src/testFixtures/java/org/apache/ignite/distributed/ItTxTestCluster.java @@ -585,7 +585,8 @@ protected TxManagerImpl newTxManager( partitionOperationsExecutor, resourcesRegistry, transactionInflights, - lowWatermark + lowWatermark, + executor ); } @@ -1074,7 +1075,8 @@ private void initializeClientTxComponents() { partitionOperationsExecutor, resourceRegistry, clientTransactionInflights, - lowWatermark + lowWatermark, + executor ); clientResourceVacuumManager = new ResourceVacuumManager( diff --git a/modules/table/src/testFixtures/java/org/apache/ignite/internal/table/TxAbstractTest.java b/modules/table/src/testFixtures/java/org/apache/ignite/internal/table/TxAbstractTest.java index 86c7e31ae19..8c3f41467a6 100644 --- a/modules/table/src/testFixtures/java/org/apache/ignite/internal/table/TxAbstractTest.java +++ b/modules/table/src/testFixtures/java/org/apache/ignite/internal/table/TxAbstractTest.java @@ -83,6 +83,7 @@ import org.apache.ignite.internal.table.distributed.replicator.PartitionReplicaListener; import org.apache.ignite.internal.testframework.IgniteTestUtils; import org.apache.ignite.internal.tx.InternalTransaction; +import org.apache.ignite.internal.tx.InternalTxOptions; import org.apache.ignite.internal.tx.Lock; import org.apache.ignite.internal.tx.LockException; import org.apache.ignite.internal.tx.LockManager; @@ -1735,7 +1736,7 @@ public void run() { } while (!stop.get() && firstErr.get() == null) { - InternalTransaction tx = clientTxManager().begin(timestampTracker, false, false); + InternalTransaction tx = clientTxManager().beginExplicitRw(timestampTracker, InternalTxOptions.defaults()); var table = accounts.recordView(); diff --git a/modules/table/src/testFixtures/java/org/apache/ignite/internal/table/TxInfrastructureTest.java b/modules/table/src/testFixtures/java/org/apache/ignite/internal/table/TxInfrastructureTest.java index 341fa32abbc..106c798e9eb 100644 --- a/modules/table/src/testFixtures/java/org/apache/ignite/internal/table/TxInfrastructureTest.java +++ b/modules/table/src/testFixtures/java/org/apache/ignite/internal/table/TxInfrastructureTest.java @@ -25,6 +25,7 @@ import java.util.Collection; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; import org.apache.ignite.distributed.ItTxTestCluster; import org.apache.ignite.internal.configuration.testframework.ConfigurationExtension; import org.apache.ignite.internal.configuration.testframework.InjectConfiguration; @@ -49,8 +50,10 @@ import org.apache.ignite.internal.schema.configuration.StorageUpdateConfiguration; import org.apache.ignite.internal.storage.MvPartitionStorage; import org.apache.ignite.internal.table.distributed.raft.PartitionListener; +import org.apache.ignite.internal.testframework.ExecutorServiceExtension; import org.apache.ignite.internal.testframework.IgniteAbstractTest; import org.apache.ignite.internal.testframework.IgniteTestUtils; +import org.apache.ignite.internal.testframework.InjectExecutorService; import org.apache.ignite.internal.tx.HybridTimestampTracker; import org.apache.ignite.internal.tx.TxManager; import org.apache.ignite.internal.tx.configuration.TransactionConfiguration; @@ -72,7 +75,7 @@ /** * Setup infrastructure for tx related test scenarios. */ -@ExtendWith({MockitoExtension.class, ConfigurationExtension.class}) +@ExtendWith({MockitoExtension.class, ConfigurationExtension.class, ExecutorServiceExtension.class}) @MockitoSettings(strictness = Strictness.LENIENT) public abstract class TxInfrastructureTest extends IgniteAbstractTest { protected static final double BALANCE_1 = 500; @@ -120,6 +123,9 @@ public abstract class TxInfrastructureTest extends IgniteAbstractTest { @InjectConfiguration protected ReplicationConfiguration replicationConfiguration; + @InjectExecutorService + protected ScheduledExecutorService commonExecutor; + protected final TestInfo testInfo; protected ItTxTestCluster txTestCluster; diff --git a/modules/table/src/testFixtures/java/org/apache/ignite/internal/table/impl/DummyInternalTableImpl.java b/modules/table/src/testFixtures/java/org/apache/ignite/internal/table/impl/DummyInternalTableImpl.java index 570282eef8d..775480661c5 100644 --- a/modules/table/src/testFixtures/java/org/apache/ignite/internal/table/impl/DummyInternalTableImpl.java +++ b/modules/table/src/testFixtures/java/org/apache/ignite/internal/table/impl/DummyInternalTableImpl.java @@ -37,6 +37,8 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.apache.ignite.distributed.TestPartitionDataStorage; @@ -106,6 +108,7 @@ import org.apache.ignite.internal.table.distributed.replicator.TransactionStateResolver; import org.apache.ignite.internal.table.distributed.schema.AlwaysSyncedSchemaSyncService; import org.apache.ignite.internal.table.distributed.storage.InternalTableImpl; +import org.apache.ignite.internal.thread.NamedThreadFactory; import org.apache.ignite.internal.tx.HybridTimestampTracker; import org.apache.ignite.internal.tx.InternalTransaction; import org.apache.ignite.internal.tx.TxManager; @@ -165,6 +168,10 @@ public class DummyInternalTableImpl extends InternalTableImpl { private static final AtomicInteger nextTableId = new AtomicInteger(10_001); + private static final ScheduledExecutorService COMMON_SCHEDULER = Executors.newSingleThreadScheduledExecutor( + new NamedThreadFactory("DummyInternalTable-common-scheduler-", true, LOG) + ); + /** * Creates a new local table. * @@ -570,7 +577,8 @@ public static TxManagerImpl txManager( new TestLocalRwTxCounter(), resourcesRegistry, transactionInflights, - new TestLowWatermark() + new TestLowWatermark(), + COMMON_SCHEDULER ); assertThat(txManager.startAsync(new ComponentContext()), willCompleteSuccessfully()); diff --git a/modules/transactions/build.gradle b/modules/transactions/build.gradle index 9ea37ab1872..ff08a3d9c2e 100644 --- a/modules/transactions/build.gradle +++ b/modules/transactions/build.gradle @@ -81,11 +81,14 @@ dependencies { integrationTestImplementation project(':ignite-schema') integrationTestImplementation project(':ignite-low-watermark') integrationTestImplementation project(':ignite-configuration-system') + integrationTestImplementation project(':ignite-client') + integrationTestImplementation project(':ignite-client-handler') integrationTestImplementation libs.jetbrains.annotations integrationTestImplementation(testFixtures(project(':ignite-core'))) integrationTestImplementation(testFixtures(project(':ignite-transactions'))) integrationTestImplementation(testFixtures(project(':ignite-sql-engine'))) integrationTestImplementation(testFixtures(project(':ignite-runner'))) + integrationTestImplementation libs.netty.transport testFixturesImplementation project(':ignite-configuration') testFixturesImplementation project(':ignite-core') diff --git a/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItClientReadOnlyTxTimeoutOneNodeTest.java b/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItClientReadOnlyTxTimeoutOneNodeTest.java new file mode 100644 index 00000000000..b7ca1c75964 --- /dev/null +++ b/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItClientReadOnlyTxTimeoutOneNodeTest.java @@ -0,0 +1,65 @@ +/* + * 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. + */ + +package org.apache.ignite.internal.tx.readonly; + +import static org.apache.ignite.internal.TestWrappers.unwrapIgniteImpl; + +import org.apache.ignite.Ignite; +import org.apache.ignite.client.IgniteClient; +import org.apache.ignite.client.handler.ClientResourceRegistry; +import org.apache.ignite.internal.client.tx.ClientLazyTransaction; +import org.apache.ignite.internal.lang.IgniteInternalCheckedException; +import org.apache.ignite.internal.tx.impl.ReadOnlyTransactionImpl; +import org.apache.ignite.tx.Transaction; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +class ItClientReadOnlyTxTimeoutOneNodeTest extends ItReadOnlyTxTimeoutOneNodeTest { + private IgniteClient client; + + @BeforeEach + void startClient() { + client = IgniteClient.builder() + .addresses("localhost:" + unwrapIgniteImpl(cluster.aliveNode()).clientAddress().port()) + .build(); + } + + @AfterEach + void closeClient() { + if (client != null) { + client.close(); + } + } + + @Override + Ignite ignite() { + return client; + } + + @Override + ReadOnlyTransactionImpl transactionImpl(Transaction tx) { + long txId = ClientLazyTransaction.get(tx).startedTx().id(); + + ClientResourceRegistry resources = unwrapIgniteImpl(cluster.aliveNode()).clientInboundMessageHandler().resources(); + try { + return resources.get(txId).get(ReadOnlyTransactionImpl.class); + } catch (IgniteInternalCheckedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItEmbeddedReadOnlyTxTimeoutOneNodeTest.java b/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItEmbeddedReadOnlyTxTimeoutOneNodeTest.java new file mode 100644 index 00000000000..9d174defc88 --- /dev/null +++ b/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItEmbeddedReadOnlyTxTimeoutOneNodeTest.java @@ -0,0 +1,35 @@ +/* + * 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. + */ + +package org.apache.ignite.internal.tx.readonly; + +import org.apache.ignite.Ignite; +import org.apache.ignite.internal.tx.impl.ReadOnlyTransactionImpl; +import org.apache.ignite.internal.wrapper.Wrappers; +import org.apache.ignite.tx.Transaction; + +class ItEmbeddedReadOnlyTxTimeoutOneNodeTest extends ItReadOnlyTxTimeoutOneNodeTest { + @Override + Ignite ignite() { + return cluster.aliveNode(); + } + + @Override + ReadOnlyTransactionImpl transactionImpl(Transaction tx) { + return Wrappers.unwrap(tx, ReadOnlyTransactionImpl.class); + } +} diff --git a/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItReadOnlyTxAndLowWatermarkTest.java b/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItReadOnlyTxAndLowWatermarkTest.java index df078a35b63..7ba4671d6c0 100644 --- a/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItReadOnlyTxAndLowWatermarkTest.java +++ b/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItReadOnlyTxAndLowWatermarkTest.java @@ -25,6 +25,7 @@ import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.is; @@ -120,8 +121,14 @@ void roTransactionNoticesTupleVersionsMissingDueToGcOnDataNodes(TransactionalRea IgniteException ex = assertThrows(IgniteException.class, () -> reader.read(coordinator, roTx)); assertThat(ex, isA(reader.sql() ? SqlException.class : TransactionException.class)); - assertThat(ex, hasToString(containsString("Read timestamp is not available anymore."))); - assertThat("Wrong error code: " + ex.codeAsString(), ex.code(), is(Transactions.TX_STALE_READ_ONLY_OPERATION_ERR)); + assertThat(ex, hasToString( + either(containsString("Read timestamp is not available anymore.")) + .or(containsString("Transaction is already finished")) + )); + assertThat("Wrong error code: " + ex.codeAsString(), ex.code(), + either(is(Transactions.TX_STALE_READ_ONLY_OPERATION_ERR)) + .or(is(Transactions.TX_ALREADY_FINISHED_ERR)) + ); } private void updateDataAvailabilityTimeToShortPeriod() { diff --git a/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItReadOnlyTxTimeoutOneNodeTest.java b/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItReadOnlyTxTimeoutOneNodeTest.java new file mode 100644 index 00000000000..c03ed59f1f2 --- /dev/null +++ b/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/readonly/ItReadOnlyTxTimeoutOneNodeTest.java @@ -0,0 +1,72 @@ +/* + * 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. + */ + +package org.apache.ignite.internal.tx.readonly; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.ignite.Ignite; +import org.apache.ignite.internal.ClusterPerTestIntegrationTest; +import org.apache.ignite.internal.tx.impl.ReadOnlyTransactionImpl; +import org.apache.ignite.table.Table; +import org.apache.ignite.tx.Transaction; +import org.apache.ignite.tx.TransactionException; +import org.apache.ignite.tx.TransactionOptions; +import org.junit.jupiter.api.Test; + +abstract class ItReadOnlyTxTimeoutOneNodeTest extends ClusterPerTestIntegrationTest { + private static final String TABLE_NAME = "TEST"; + + @Override + protected int initialNodes() { + return 1; + } + + abstract Ignite ignite(); + + abstract ReadOnlyTransactionImpl transactionImpl(Transaction tx); + + @Test + void roTransactionTimesOut() throws Exception { + Ignite ignite = ignite(); + + ignite.sql().executeScript("CREATE TABLE " + TABLE_NAME + " (ID INT PRIMARY KEY, VAL VARCHAR)"); + + Table table = ignite.tables().table(TABLE_NAME); + + Transaction roTx = ignite.transactions().begin(new TransactionOptions().readOnly(true).timeoutMillis(100)); + + // Make sure the RO tx actually begins on the server (as thin client transactions are lazy). + doGetOn(table, roTx); + + assertTrue( + waitForCondition(() -> transactionImpl(roTx).isFinishingOrFinished(), SECONDS.toMillis(10)), + "Transaction should have been finished due to timeout" + ); + + assertThrows(TransactionException.class, () -> doGetOn(table, roTx)); + // TODO: uncomment the following assert after IGNITE-24233 is fixed. + // assertThrows(TransactionException.class, roTx::commit); + } + + private static void doGetOn(Table table, Transaction tx) { + table.keyValueView(Integer.class, String.class).get(tx, 1); + } +} diff --git a/modules/transactions/src/integrationTest/java/org/apache/ignite/tx/distributed/ItTxResourcesVacuumTest.java b/modules/transactions/src/integrationTest/java/org/apache/ignite/tx/distributed/ItTxResourcesVacuumTest.java index 851a69461b0..2c1e16b7354 100644 --- a/modules/transactions/src/integrationTest/java/org/apache/ignite/tx/distributed/ItTxResourcesVacuumTest.java +++ b/modules/transactions/src/integrationTest/java/org/apache/ignite/tx/distributed/ItTxResourcesVacuumTest.java @@ -786,7 +786,11 @@ public void testRoReadTheCorrectDataInBetween() { } private static Transaction beginReadOnlyTx(IgniteImpl node) { - return node.transactions().begin(new TransactionOptions().readOnly(true)); + return node.transactions().begin( + new TransactionOptions() + .readOnly(true) + .timeoutMillis(Long.MAX_VALUE) + ); } /** diff --git a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/InternalTxOptions.java b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/InternalTxOptions.java new file mode 100644 index 00000000000..36d1ba62068 --- /dev/null +++ b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/InternalTxOptions.java @@ -0,0 +1,79 @@ +/* + * 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. + */ + +package org.apache.ignite.internal.tx; + +/** + * Transaction options for internal use. + */ +public class InternalTxOptions { + private static final InternalTxOptions DEFAULT_OPTIONS = builder().build(); + + /** + * Transaction priority. The priority is used to resolve conflicts between transactions. The higher priority is + * the more likely the transaction will win the conflict. + */ + private final TxPriority priority; + + /** Transaction timeout. 0 means 'use default timeout'. */ + private final long timeoutMillis; + + private InternalTxOptions(TxPriority priority, long timeoutMillis) { + this.priority = priority; + this.timeoutMillis = timeoutMillis; + } + + public static Builder builder() { + return new Builder(); + } + + public static InternalTxOptions defaults() { + return DEFAULT_OPTIONS; + } + + public static InternalTxOptions defaultsWithPriority(TxPriority priority) { + return builder().priority(priority).build(); + } + + public TxPriority priority() { + return priority; + } + + public long timeoutMillis() { + return timeoutMillis; + } + + /** Builder for InternalTxOptions. */ + public static class Builder { + private TxPriority priority = TxPriority.NORMAL; + private long timeoutMillis = 0; + + public Builder priority(TxPriority priority) { + this.priority = priority; + return this; + } + + public Builder timeoutMillis(long timeoutMillis) { + this.timeoutMillis = timeoutMillis; + return this; + } + + public InternalTxOptions build() { + return new InternalTxOptions(priority, timeoutMillis); + } + } +} diff --git a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/TxManager.java b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/TxManager.java index d21f77d80e7..3cfa0d89eb6 100644 --- a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/TxManager.java +++ b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/TxManager.java @@ -24,10 +24,8 @@ import java.util.function.Function; import org.apache.ignite.internal.hlc.HybridTimestamp; import org.apache.ignite.internal.lang.IgniteBiTuple; -import org.apache.ignite.internal.lang.IgniteInternalException; import org.apache.ignite.internal.manager.IgniteComponent; import org.apache.ignite.internal.replicator.TablePartitionId; -import org.apache.ignite.lang.ErrorGroups.Transactions; import org.apache.ignite.network.ClusterNode; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -37,45 +35,72 @@ */ public interface TxManager extends IgniteComponent { /** - * Starts a read-write transaction coordinated by a local node. + * Starts an implicit read-write transaction coordinated by a local node. * * @param timestampTracker Observable timestamp tracker is used to track a timestamp for either read-write or read-only * transaction execution. The tracker is also used to determine the read timestamp for read-only transactions. - * @param implicit Whether the transaction is implicit or not. * @return The transaction. */ - InternalTransaction begin(HybridTimestampTracker timestampTracker, boolean implicit); + default InternalTransaction beginImplicitRw(HybridTimestampTracker timestampTracker) { + return beginImplicit(timestampTracker, false); + } /** - * Starts either read-write or read-only transaction, depending on {@code readOnly} parameter value. The transaction has - * {@link TxPriority#NORMAL} priority. + * Starts an implicit read-only transaction coordinated by a local node. * * @param timestampTracker Observable timestamp tracker is used to track a timestamp for either read-write or read-only - * transaction execution. The tracker is also used to determine the read timestamp for read-only transactions. Each client - * should pass its own tracker to provide linearizability between read-write and read-only transactions started by this client. - * @param implicit Whether the transaction is implicit or not. + * transaction execution. The tracker is also used to determine the read timestamp for read-only transactions. + * @return The transaction. + */ + default InternalTransaction beginImplicitRo(HybridTimestampTracker timestampTracker) { + return beginImplicit(timestampTracker, true); + } + + /** + * Starts an implicit transaction coordinated by a local node. + * + * @param timestampTracker Observable timestamp tracker is used to track a timestamp for either read-write or read-only + * transaction execution. The tracker is also used to determine the read timestamp for read-only transactions. * @param readOnly {@code true} in order to start a read-only transaction, {@code false} in order to start read-write one. - * Calling begin with readOnly {@code false} is an equivalent of TxManager#begin(). - * @return The started transaction. - * @throws IgniteInternalException with {@link Transactions#TX_READ_ONLY_TOO_OLD_ERR} if transaction much older than the data - * available in the tables. + * @return The transaction. + */ + InternalTransaction beginImplicit(HybridTimestampTracker timestampTracker, boolean readOnly); + + /** + * Starts an explicit read-write transaction coordinated by a local node. + * + * @param timestampTracker Observable timestamp tracker is used to track a timestamp for either read-write or read-only + * transaction execution. The tracker is also used to determine the read timestamp for read-only transactions. + * @param options Transaction options. + * @return The transaction. + */ + default InternalTransaction beginExplicitRw(HybridTimestampTracker timestampTracker, InternalTxOptions options) { + return beginExplicit(timestampTracker, false, options); + } + + /** + * Starts an explicit read-only transaction coordinated by a local node. + * + * @param timestampTracker Observable timestamp tracker is used to track a timestamp for either read-write or read-only + * transaction execution. The tracker is also used to determine the read timestamp for read-only transactions. + * @param options Transaction options. + * @return The transaction. */ - InternalTransaction begin(HybridTimestampTracker timestampTracker, boolean implicit, boolean readOnly); + default InternalTransaction beginExplicitRo(HybridTimestampTracker timestampTracker, InternalTxOptions options) { + return beginExplicit(timestampTracker, true, options); + } /** - * Starts either read-write or read-only transaction, depending on {@code readOnly} parameter value. + * Starts either read-write or read-only explicit transaction, depending on {@code readOnly} parameter value. * * @param timestampTracker Observable timestamp tracker is used to track a timestamp for either read-write or read-only * transaction execution. The tracker is also used to determine the read timestamp for read-only transactions. Each client * should pass its own tracker to provide linearizability between read-write and read-only transactions started by this client. - * @param implicit Whether the transaction is implicit or not. * @param readOnly {@code true} in order to start a read-only transaction, {@code false} in order to start read-write one. - * Calling begin with readOnly {@code false} is an equivalent of TxManager#begin(). - * @param priority Transaction priority. The priority is used to resolve conflicts between transactions. The higher priority is - * the more likely the transaction will win the conflict. + * @param txOptions Options. * @return The started transaction. */ - InternalTransaction begin(HybridTimestampTracker timestampTracker, boolean implicit, boolean readOnly, TxPriority priority); + InternalTransaction beginExplicit(HybridTimestampTracker timestampTracker, boolean readOnly, InternalTxOptions txOptions); /** * Returns a transaction state meta. diff --git a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/configuration/TransactionConfigurationSchema.java b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/configuration/TransactionConfigurationSchema.java index 0a619c7b905..47bf5b8b447 100644 --- a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/configuration/TransactionConfigurationSchema.java +++ b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/configuration/TransactionConfigurationSchema.java @@ -31,13 +31,18 @@ public class TransactionConfigurationSchema { /** Default checking transaction interval. */ public static final long DEFAULT_ABANDONED_CHECK_TS = 5_000; - /** Checking transaction interval. */ + /** How often abandoned transactions are searched for (milliseconds). */ @Range(min = 0) @Value(hasDefault = true) public final long abandonedCheckTs = DEFAULT_ABANDONED_CHECK_TS; - /** Timeout for implicit transactions. */ - @Range(min = 0) + /** Default transaction timeout (milliseconds). */ + @Range(min = 1) + @Value(hasDefault = true) + public final long timeout = 10_000; + + /** Timeout for implicit transactions (milliseconds). */ + @Range(min = 1) @Value(hasDefault = true) public final long implicitTransactionTimeout = 3_000; diff --git a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/IgniteTransactionsImpl.java b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/IgniteTransactionsImpl.java index f80cb12c9da..f29a5993009 100644 --- a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/IgniteTransactionsImpl.java +++ b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/IgniteTransactionsImpl.java @@ -19,6 +19,8 @@ import java.util.concurrent.CompletableFuture; import org.apache.ignite.internal.tx.HybridTimestampTracker; +import org.apache.ignite.internal.tx.InternalTransaction; +import org.apache.ignite.internal.tx.InternalTxOptions; import org.apache.ignite.internal.tx.TxManager; import org.apache.ignite.internal.tx.TxPriority; import org.apache.ignite.tx.IgniteTransactions; @@ -48,12 +50,18 @@ public IgniteTransactionsImpl(TxManager txManager, HybridTimestampTracker observ /** {@inheritDoc} */ @Override public Transaction begin(@Nullable TransactionOptions options) { - if (options != null && options.timeoutMillis() != 0) { + if (options != null && options.timeoutMillis() != 0 && !options.readOnly()) { // TODO: IGNITE-15936. - throw new UnsupportedOperationException("Timeouts are not supported yet"); + throw new UnsupportedOperationException("Timeouts are not supported yet for RW transactions."); } - return txManager.begin(observableTimestampTracker, false, options != null && options.readOnly()); + InternalTxOptions internalTxOptions = options == null + ? InternalTxOptions.defaults() + : InternalTxOptions.builder() + .timeoutMillis(options.timeoutMillis()) + .build(); + + return txManager.beginExplicit(observableTimestampTracker, options != null && options.readOnly(), internalTxOptions); } /** {@inheritDoc} */ @@ -62,8 +70,18 @@ public CompletableFuture beginAsync(@Nullable TransactionOptions op return CompletableFuture.completedFuture(begin(options)); } + /** + * Begins a transaction. + * + * @param readOnly {@code true} in order to start a read-only transaction, {@code false} in order to start read-write one. + * @return The started transaction. + */ + public InternalTransaction beginImplicit(boolean readOnly) { + return txManager.beginImplicit(observableTimestampTracker, readOnly); + } + @TestOnly public Transaction beginWithPriority(boolean readOnly, TxPriority priority) { - return txManager.begin(observableTimestampTracker, false, readOnly, priority); + return txManager.beginExplicit(observableTimestampTracker, readOnly, InternalTxOptions.defaultsWithPriority(priority)); } } diff --git a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TransactionExpirationRegistry.java b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TransactionExpirationRegistry.java new file mode 100644 index 00000000000..b3c0351958d --- /dev/null +++ b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TransactionExpirationRegistry.java @@ -0,0 +1,155 @@ +/* + * 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. + */ + +package org.apache.ignite.internal.tx.impl; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.apache.ignite.internal.logger.IgniteLogger; +import org.apache.ignite.internal.logger.Loggers; +import org.apache.ignite.internal.tx.InternalTransaction; + +class TransactionExpirationRegistry { + private static final IgniteLogger LOG = Loggers.forClass(TransactionExpirationRegistry.class); + + /** + * Map from expiration timestamp (number of millis since Unix epoch) to transactions expiring at the timestamp. + * Each value is either a transaction or a set of at least 2 transactions. + */ + private final NavigableMap txsByExpirationTime = new ConcurrentSkipListMap<>(); + + /** Map from registered transaction to its expiration timestamp. */ + private final Map expirationTimeByTx = new ConcurrentHashMap<>(); + + private final ReadWriteLock watermarkLock = new ReentrantReadWriteLock(); + + /** Watermark at which expiration has already happened (millis since Unix epoch). */ + private volatile long watermark = Long.MIN_VALUE; + + void register(InternalTransaction tx, long txExpirationTime) { + if (isExpired(txExpirationTime)) { + abortTransaction(tx); + return; + } + + watermarkLock.readLock().lock(); + + try { + if (isExpired(txExpirationTime)) { + abortTransaction(tx); + return; + } + + txsByExpirationTime.compute( + txExpirationTime, + (k, txOrSet) -> { + if (txOrSet == null) { + return tx; + } + + Set txsExpiringAtTs; + if (txOrSet instanceof Set) { + txsExpiringAtTs = (Set) txOrSet; + } else { + txsExpiringAtTs = new HashSet<>(); + txsExpiringAtTs.add((InternalTransaction) txOrSet); + } + + txsExpiringAtTs.add(tx); + + return txsExpiringAtTs; + } + ); + + expirationTimeByTx.put(tx, txExpirationTime); + } finally { + watermarkLock.readLock().unlock(); + } + } + + private boolean isExpired(long expirationTime) { + return expirationTime <= watermark; + } + + private static void abortTransaction(InternalTransaction tx) { + tx.rollbackAsync().whenComplete((res, ex) -> { + if (ex != null) { + LOG.error("Transaction abort due to timeout failed [txId={}]", ex, tx.id()); + } + }); + } + + void expireUpTo(long expirationTime) { + List transactionsAndSetsToExpire; + + watermarkLock.writeLock().lock(); + + try { + NavigableMap headMap = txsByExpirationTime.headMap(expirationTime, true); + transactionsAndSetsToExpire = new ArrayList<>(headMap.values()); + headMap.clear(); + + watermark = expirationTime; + } finally { + watermarkLock.writeLock().unlock(); + } + + for (Object txOrSet : transactionsAndSetsToExpire) { + if (txOrSet instanceof Set) { + for (InternalTransaction tx : (Set) txOrSet) { + expirationTimeByTx.remove(tx); + abortTransaction(tx); + } + } else { + InternalTransaction tx = (InternalTransaction) txOrSet; + + expirationTimeByTx.remove(tx); + abortTransaction(tx); + } + } + } + + void abortAllRegistered() { + expireUpTo(Long.MAX_VALUE); + } + + void unregister(InternalTransaction tx) { + Long expirationTime = expirationTimeByTx.remove(tx); + + if (expirationTime != null) { + txsByExpirationTime.computeIfPresent(expirationTime, (k, txOrSet) -> { + if (txOrSet instanceof Set) { + Set set = (Set) txOrSet; + + set.remove(tx); + + return set.size() == 1 ? set.iterator().next() : set; + } else { + return null; + } + }); + } + } +} diff --git a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TxManagerImpl.java b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TxManagerImpl.java index abcd0a8e1d7..54ba6d4804b 100644 --- a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TxManagerImpl.java +++ b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TxManagerImpl.java @@ -21,6 +21,7 @@ import static java.util.concurrent.CompletableFuture.failedFuture; import static java.util.concurrent.CompletableFuture.runAsync; import static java.util.concurrent.CompletableFuture.supplyAsync; +import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.function.Function.identity; import static org.apache.ignite.internal.lang.IgniteStringFormatter.format; import static org.apache.ignite.internal.thread.ThreadOperation.STORAGE_READ; @@ -49,6 +50,8 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -88,13 +91,13 @@ import org.apache.ignite.internal.thread.IgniteThreadFactory; import org.apache.ignite.internal.tx.HybridTimestampTracker; import org.apache.ignite.internal.tx.InternalTransaction; +import org.apache.ignite.internal.tx.InternalTxOptions; import org.apache.ignite.internal.tx.LocalRwTxCounter; import org.apache.ignite.internal.tx.LockManager; import org.apache.ignite.internal.tx.MismatchingTransactionOutcomeInternalException; import org.apache.ignite.internal.tx.TransactionMeta; import org.apache.ignite.internal.tx.TransactionResult; import org.apache.ignite.internal.tx.TxManager; -import org.apache.ignite.internal.tx.TxPriority; import org.apache.ignite.internal.tx.TxState; import org.apache.ignite.internal.tx.TxStateMeta; import org.apache.ignite.internal.tx.TxStateMetaFinishing; @@ -197,10 +200,16 @@ public class TxManagerImpl implements TxManager, NetworkMessageHandler, SystemVi private final ReplicaService replicaService; + private final ScheduledExecutorService commonScheduler; + private final TransactionsViewProvider txViewProvider = new TransactionsViewProvider(); private volatile PersistentTxStateVacuumizer persistentTxStateVacuumizer; + private final TransactionExpirationRegistry transactionExpirationRegistry = new TransactionExpirationRegistry(); + + private volatile @Nullable ScheduledFuture transactionExpirationJobFuture; + /** * Test-only constructor. * @@ -230,7 +239,8 @@ public TxManagerImpl( LocalRwTxCounter localRwTxCounter, RemotelyTriggeredResourceRegistry resourcesRegistry, TransactionInflights transactionInflights, - LowWatermark lowWatermark + LowWatermark lowWatermark, + ScheduledExecutorService commonScheduler ) { this( clusterService.nodeName(), @@ -247,7 +257,8 @@ public TxManagerImpl( ForkJoinPool.commonPool(), resourcesRegistry, transactionInflights, - lowWatermark + lowWatermark, + commonScheduler ); } @@ -284,7 +295,8 @@ public TxManagerImpl( Executor partitionOperationsExecutor, RemotelyTriggeredResourceRegistry resourcesRegistry, TransactionInflights transactionInflights, - LowWatermark lowWatermark + LowWatermark lowWatermark, + ScheduledExecutorService commonScheduler ) { this.txConfig = txConfig; this.lockManager = lockManager; @@ -301,6 +313,7 @@ public TxManagerImpl( this.transactionInflights = transactionInflights; this.lowWatermark = lowWatermark; this.replicaService = replicaService; + this.commonScheduler = commonScheduler; placementDriverHelper = new PlacementDriverHelper(placementDriver, clockService); @@ -310,7 +323,7 @@ public TxManagerImpl( cpus, cpus, 100, - TimeUnit.MILLISECONDS, + MILLISECONDS, new LinkedBlockingQueue<>(), IgniteThreadFactory.create(nodeName, "tx-async-write-intent", LOG, STORAGE_READ, STORAGE_WRITE) ); @@ -371,24 +384,32 @@ private CompletableFuture primaryReplicaExpiredListener(PrimaryReplicaE } @Override - public InternalTransaction begin(HybridTimestampTracker timestampTracker, boolean implicit) { - return begin(timestampTracker, implicit, false); + public InternalTransaction beginImplicit(HybridTimestampTracker timestampTracker, boolean readOnly) { + return begin(timestampTracker, true, readOnly, InternalTxOptions.defaults()); } @Override - public InternalTransaction begin(HybridTimestampTracker timestampTracker, boolean implicit, boolean readOnly) { - return begin(timestampTracker, implicit, readOnly, TxPriority.NORMAL); + public InternalTransaction beginExplicit(HybridTimestampTracker timestampTracker, boolean readOnly, InternalTxOptions txOptions) { + return begin(timestampTracker, false, readOnly, txOptions); } - @Override - public InternalTransaction begin( + private InternalTransaction begin( HybridTimestampTracker timestampTracker, boolean implicit, boolean readOnly, - TxPriority priority + InternalTxOptions options + ) { + return inBusyLock(busyLock, () -> beginBusy(timestampTracker, implicit, readOnly, options)); + } + + private InternalTransaction beginBusy( + HybridTimestampTracker timestampTracker, + boolean implicit, + boolean readOnly, + InternalTxOptions options ) { HybridTimestamp beginTimestamp = readOnly ? clockService.now() : createBeginTimestampWithIncrementRwTxCounter(); - UUID txId = transactionIdGenerator.transactionIdFor(beginTimestamp, priority); + UUID txId = transactionIdGenerator.transactionIdFor(beginTimestamp, options.priority()); startedTxs.add(1); @@ -396,8 +417,18 @@ public InternalTransaction begin( txStateVolatileStorage.initialize(txId, localNodeId); return new ReadWriteTransactionImpl(this, timestampTracker, txId, localNodeId, implicit); + } else { + return beginReadOnlyTransaction(timestampTracker, beginTimestamp, txId, implicit, options); } + } + private ReadOnlyTransactionImpl beginReadOnlyTransaction( + HybridTimestampTracker timestampTracker, + HybridTimestamp beginTimestamp, + UUID txId, + boolean implicit, + InternalTxOptions options + ) { HybridTimestamp observableTimestamp = timestampTracker.get(); HybridTimestamp readTimestamp = observableTimestamp != null @@ -415,17 +446,57 @@ public InternalTransaction begin( try { CompletableFuture txFuture = new CompletableFuture<>(); + + var transaction = new ReadOnlyTransactionImpl(this, timestampTracker, txId, localNodeId, implicit, readTimestamp, txFuture); + + // Implicit transactions are finished as soon as their operation/query is finished, they cannot be abandoned, so there is + // no need to register them. + // TODO: https://issues.apache.org/jira/browse/IGNITE-24229 - schedule expiration for multi-key implicit transactions? + boolean scheduleExpiration = !implicit; + + if (scheduleExpiration) { + transactionExpirationRegistry.register(transaction, roExpirationPhysicalTimeFor(beginTimestamp, options)); + } + txFuture.whenComplete((unused, throwable) -> { lowWatermark.unlock(txId); + + // We only register explicit transactions, so we only unregister them as well. + if (scheduleExpiration) { + transactionExpirationRegistry.unregister(transaction); + } }); - return new ReadOnlyTransactionImpl(this, timestampTracker, txId, localNodeId, implicit, readTimestamp, txFuture); + return transaction; } catch (Throwable t) { lowWatermark.unlock(txId); throw t; } } + private long roExpirationPhysicalTimeFor(HybridTimestamp beginTimestamp, InternalTxOptions options) { + long effectiveTimeoutMillis = options.timeoutMillis() == 0 ? defaultTransactionTimeoutMillis() : options.timeoutMillis(); + return sumWithSaturation(beginTimestamp.getPhysical(), effectiveTimeoutMillis); + } + + private static long sumWithSaturation(long a, long b) { + assert a >= 0 : a; + assert b >= 0 : b; + + long sum = a + b; + + if (sum < 0) { + // Overflow. + return Long.MAX_VALUE; + } else { + return sum; + } + } + + private long defaultTransactionTimeoutMillis() { + return txConfig.timeout().value(); + } + /** * Current read timestamp, for calculation of read timestamp of read-only transactions. * @@ -787,10 +858,23 @@ public CompletableFuture startAsync(ComponentContext componentContext) { placementDriver.listen(PrimaryReplicaEvent.PRIMARY_REPLICA_ELECTED, primaryReplicaElectedListener); + transactionExpirationJobFuture = commonScheduler.scheduleAtFixedRate(this::expireTransactionsUpToNow, 1000, 1000, MILLISECONDS); + return nullCompletedFuture(); }); } + private void expireTransactionsUpToNow() { + HybridTimestamp expirationTime = null; + + try { + expirationTime = clockService.current(); + transactionExpirationRegistry.expireUpTo(expirationTime.getPhysical()); + } catch (Throwable t) { + LOG.error("Could not expire transactions up to {}", t, expirationTime); + } + } + @Override public void beforeNodeStop() { orphanDetector.stop(); @@ -812,6 +896,13 @@ public CompletableFuture stopAsync(ComponentContext componentContext) { placementDriver.removeListener(PrimaryReplicaEvent.PRIMARY_REPLICA_ELECTED, primaryReplicaElectedListener); + ScheduledFuture expirationJobFuture = transactionExpirationJobFuture; + if (expirationJobFuture != null) { + expirationJobFuture.cancel(false); + } + + transactionExpirationRegistry.abortAllRegistered(); + shutdownAndAwaitTermination(writeIntentSwitchPool, 10, TimeUnit.SECONDS); return nullCompletedFuture(); diff --git a/modules/transactions/src/test/java/org/apache/ignite/internal/tx/TxManagerTest.java b/modules/transactions/src/test/java/org/apache/ignite/internal/tx/TxManagerTest.java index e27822a140f..e3b17c18dbc 100644 --- a/modules/transactions/src/test/java/org/apache/ignite/internal/tx/TxManagerTest.java +++ b/modules/transactions/src/test/java/org/apache/ignite/internal/tx/TxManagerTest.java @@ -55,6 +55,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.LongSupplier; import java.util.stream.Stream; @@ -78,7 +79,9 @@ import org.apache.ignite.internal.replicator.ReplicaService; import org.apache.ignite.internal.replicator.TablePartitionId; import org.apache.ignite.internal.replicator.exception.PrimaryReplicaMissException; +import org.apache.ignite.internal.testframework.ExecutorServiceExtension; import org.apache.ignite.internal.testframework.IgniteAbstractTest; +import org.apache.ignite.internal.testframework.InjectExecutorService; import org.apache.ignite.internal.tx.configuration.TransactionConfiguration; import org.apache.ignite.internal.tx.impl.HeapLockManager; import org.apache.ignite.internal.tx.impl.PrimaryReplicaExpiredException; @@ -110,6 +113,7 @@ * Basic tests for a transaction manager. */ @ExtendWith(ConfigurationExtension.class) +@ExtendWith(ExecutorServiceExtension.class) public class TxManagerTest extends IgniteAbstractTest { private static final ClusterNode LOCAL_NODE = new ClusterNodeImpl(randomUUID(), "local", new NetworkAddress("127.0.0.1", 2004), null); @@ -138,6 +142,9 @@ public class TxManagerTest extends IgniteAbstractTest { @InjectConfiguration private SystemLocalConfiguration systemLocalConfiguration; + @InjectExecutorService + private ScheduledExecutorService commonScheduler; + private final LocalRwTxCounter localRwTxCounter = spy(new TestLocalRwTxCounter()); private final TestLowWatermark lowWatermark = spy(new TestLowWatermark()); @@ -170,7 +177,8 @@ public void setup() { localRwTxCounter, resourceRegistry, transactionInflights, - lowWatermark + lowWatermark, + commonScheduler ); assertThat(txManager.startAsync(new ComponentContext()), willCompleteSuccessfully()); @@ -195,10 +203,10 @@ public void tearDown() { @Test public void testBegin() { - InternalTransaction tx0 = txManager.begin(hybridTimestampTracker, false); - InternalTransaction tx1 = txManager.begin(hybridTimestampTracker, false); - InternalTransaction tx2 = txManager.begin(hybridTimestampTracker, false, true); - InternalTransaction tx3 = txManager.begin(hybridTimestampTracker, false, true, TxPriority.NORMAL); + InternalTransaction tx0 = txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); + InternalTransaction tx1 = txManager.beginExplicitRo(hybridTimestampTracker, InternalTxOptions.defaults()); + InternalTransaction tx2 = txManager.beginImplicitRw(hybridTimestampTracker); + InternalTransaction tx3 = txManager.beginImplicitRo(hybridTimestampTracker); assertNotNull(tx0.id()); assertNotNull(tx1.id()); @@ -206,8 +214,8 @@ public void testBegin() { assertNotNull(tx3.id()); assertFalse(tx0.isReadOnly()); - assertFalse(tx1.isReadOnly()); - assertTrue(tx2.isReadOnly()); + assertTrue(tx1.isReadOnly()); + assertFalse(tx2.isReadOnly()); assertTrue(tx3.isReadOnly()); } @@ -217,7 +225,7 @@ public void testEnlist() { assertEquals(LOCAL_NODE.address(), addr); - InternalTransaction tx = txManager.begin(hybridTimestampTracker, false); + InternalTransaction tx = txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); TablePartitionId tablePartitionId = new TablePartitionId(1, 0); @@ -247,8 +255,10 @@ void testCreateNewRoTxAfterUpdateLowerWatermark() { assertThat(lowWatermark.updateAndNotify(new HybridTimestamp(10_000, 11)), willSucceedFast()); - IgniteInternalException exception = - assertThrows(IgniteInternalException.class, () -> txManager.begin(hybridTimestampTracker, false, true)); + IgniteInternalException exception = assertThrows( + IgniteInternalException.class, + () -> txManager.beginExplicitRo(hybridTimestampTracker, InternalTxOptions.defaults()) + ); assertEquals(Transactions.TX_READ_ONLY_TOO_OLD_ERR, exception.code()); } @@ -258,12 +268,12 @@ void testUpdateLowerWatermark() { // Let's check the absence of transactions. assertThat(lowWatermark.updateAndNotify(clockService.now()), willSucceedFast()); - InternalTransaction rwTx0 = txManager.begin(hybridTimestampTracker, false); + InternalTransaction rwTx0 = txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); hybridTimestampTracker.update(clockService.now()); - InternalTransaction roTx0 = txManager.begin(hybridTimestampTracker, false, true); - InternalTransaction roTx1 = txManager.begin(hybridTimestampTracker, false, true); + InternalTransaction roTx0 = txManager.beginExplicitRo(hybridTimestampTracker, InternalTxOptions.defaults()); + InternalTransaction roTx1 = txManager.beginExplicitRo(hybridTimestampTracker, InternalTxOptions.defaults()); CompletableFuture readOnlyTxsFuture = lowWatermark.updateAndNotify(roTx1.readTimestamp()); assertFalse(readOnlyTxsFuture.isDone()); @@ -278,8 +288,8 @@ void testUpdateLowerWatermark() { assertTrue(readOnlyTxsFuture.isDone()); // Let's check only RW transactions. - txManager.begin(hybridTimestampTracker, false); - txManager.begin(hybridTimestampTracker, false); + txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); + txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); assertThat(lowWatermark.updateAndNotify(clockService.now()), willSucceedFast()); } @@ -295,7 +305,7 @@ public void testRepeatedCommitRollbackAfterCommit() throws Exception { when(replicaService.invoke(anyString(), any(TxFinishReplicaRequest.class))) .thenReturn(completedFuture(new TransactionResult(TxState.COMMITTED, commitTimestamp))); - InternalTransaction tx = txManager.begin(hybridTimestampTracker, false); + InternalTransaction tx = txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); TablePartitionId tablePartitionId1 = new TablePartitionId(1, 0); @@ -316,7 +326,7 @@ public void testRepeatedCommitRollbackAfterRollback() throws Exception { when(replicaService.invoke(anyString(), any(TxFinishReplicaRequest.class))) .thenReturn(completedFuture(new TransactionResult(TxState.ABORTED, null))); - InternalTransaction tx = txManager.begin(hybridTimestampTracker, false); + InternalTransaction tx = txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); TablePartitionId tablePartitionId1 = new TablePartitionId(1, 0); @@ -344,7 +354,7 @@ void testRepeatedCommitRollbackAfterCommitWithException() throws Exception { ) ))); - InternalTransaction tx = txManager.begin(hybridTimestampTracker, false); + InternalTransaction tx = txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); TablePartitionId tablePartitionId1 = new TablePartitionId(1, 0); @@ -372,7 +382,7 @@ public void testRepeatedCommitRollbackAfterRollbackWithException() throws Except ) ))); - InternalTransaction tx = txManager.begin(hybridTimestampTracker, false); + InternalTransaction tx = txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); TablePartitionId tablePartitionId1 = new TablePartitionId(1, 0); @@ -389,12 +399,12 @@ public void testRepeatedCommitRollbackAfterRollbackWithException() throws Except @ParameterizedTest @ValueSource(booleans = {true, false}) - public void testTestOnlyPendingCommit(boolean startReadOnlyTransaction) { + public void testOnlyPendingCommit(boolean startReadOnlyTransaction) { assertEquals(0, txManager.pending()); assertEquals(0, txManager.finished()); // Start transaction. - InternalTransaction tx = txManager.begin(hybridTimestampTracker, false, true); + InternalTransaction tx = txManager.beginExplicitRo(hybridTimestampTracker, InternalTxOptions.defaults()); assertEquals(1, txManager.pending()); assertEquals(0, txManager.finished()); @@ -416,14 +426,14 @@ public void testTestOnlyPendingCommit(boolean startReadOnlyTransaction) { @ParameterizedTest @ValueSource(booleans = {true, false}) - public void testTestOnlyPendingRollback(boolean startReadOnlyTransaction) { + public void testOnlyPendingRollback(boolean startReadOnlyTransaction) { assertEquals(0, txManager.pending()); assertEquals(0, txManager.finished()); // Start transaction. InternalTransaction tx = - startReadOnlyTransaction ? txManager.begin(hybridTimestampTracker, false, true) - : txManager.begin(hybridTimestampTracker, false); + startReadOnlyTransaction ? txManager.beginExplicitRo(hybridTimestampTracker, InternalTxOptions.defaults()) + : txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); assertEquals(1, txManager.pending()); assertEquals(0, txManager.finished()); @@ -451,14 +461,14 @@ public void testObservableTimestamp() { HybridTimestamp now = clockService.now(); - InternalTransaction tx = txManager.begin(hybridTimestampTracker, false, true); + InternalTransaction tx = txManager.beginExplicitRo(hybridTimestampTracker, InternalTxOptions.defaults()); assertTrue(abs(now.getPhysical() - tx.readTimestamp().getPhysical()) > compareThreshold); tx.commit(); hybridTimestampTracker.update(now); - tx = txManager.begin(hybridTimestampTracker, false, true); + tx = txManager.beginExplicitRo(hybridTimestampTracker, InternalTxOptions.defaults()); assertTrue(abs(now.getPhysical() - tx.readTimestamp().getPhysical()) < compareThreshold); tx.commit(); @@ -472,7 +482,7 @@ public void testObservableTimestamp() { hybridTimestampTracker.update(timestampInPast); - tx = txManager.begin(hybridTimestampTracker, false, true); + tx = txManager.beginExplicitRo(hybridTimestampTracker, InternalTxOptions.defaults()); long readTime = now.getPhysical() - idleSafeTimePropagationPeriodMsSupplier.getAsLong() - clockService.maxClockSkewMillis(); @@ -489,7 +499,7 @@ public void testObservableTimestampLocally() { HybridTimestamp now = clockService.now(); - InternalTransaction tx = txManager.begin(hybridTimestampTracker, false, true); + InternalTransaction tx = txManager.beginExplicitRo(hybridTimestampTracker, InternalTxOptions.defaults()); HybridTimestamp firstReadTs = tx.readTimestamp(); @@ -499,7 +509,7 @@ public void testObservableTimestampLocally() { + idleSafeTimePropagationPeriodMsSupplier.getAsLong() + clockService.maxClockSkewMillis()); tx.commit(); - tx = txManager.begin(hybridTimestampTracker, false, true); + tx = txManager.beginExplicitRo(hybridTimestampTracker, InternalTxOptions.defaults()); assertTrue(firstReadTs.compareTo(tx.readTimestamp()) <= 0); @@ -611,7 +621,7 @@ public void testExpiredExceptionDoesNotShadeResponseExceptions() { @Test public void testOnlyPrimaryExpirationAffectsTransaction() { // Prepare transaction. - InternalTransaction tx = txManager.begin(hybridTimestampTracker, false); + InternalTransaction tx = txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); ClusterNode node = mock(ClusterNode.class); @@ -691,7 +701,7 @@ public void testFinishExpiredWithDifferentEnlistmentConsistencyToken() { @ParameterizedTest(name = "readOnly = {0}") @ValueSource(booleans = {true, false}) void testIncrementLocalRwTxCounterOnBeginTransaction(boolean readOnly) { - InternalTransaction tx = txManager.begin(hybridTimestampTracker, false, readOnly); + InternalTransaction tx = txManager.beginExplicit(hybridTimestampTracker, readOnly, InternalTxOptions.defaults()); VerificationMode verificationMode = readOnly ? never() : times(1); @@ -702,7 +712,7 @@ void testIncrementLocalRwTxCounterOnBeginTransaction(boolean readOnly) { @ParameterizedTest(name = "readOnly = {0}, commit = {1}") @MethodSource("txTypeAndWayCompleteTx") void testDecrementLocalRwTxCounterOnCompleteTransaction(boolean readOnly, boolean commit) { - InternalTransaction tx = txManager.begin(hybridTimestampTracker, false, readOnly); + InternalTransaction tx = txManager.beginExplicit(hybridTimestampTracker, readOnly, InternalTxOptions.defaults()); clearInvocations(localRwTxCounter); @@ -749,11 +759,11 @@ void testCreateBeginTsInsideInUpdateRwTxCount() { return result; }).when(localRwTxCounter).inUpdateRwTxCountLock(any()); - txManager.begin(hybridTimestampTracker, false, false); + txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); } private InternalTransaction prepareTransaction() { - InternalTransaction tx = txManager.begin(hybridTimestampTracker, false); + InternalTransaction tx = txManager.beginExplicitRw(hybridTimestampTracker, InternalTxOptions.defaults()); TablePartitionId tablePartitionId1 = new TablePartitionId(1, 0); diff --git a/modules/transactions/src/test/java/org/apache/ignite/internal/tx/impl/TransactionExpirationRegistryTest.java b/modules/transactions/src/test/java/org/apache/ignite/internal/tx/impl/TransactionExpirationRegistryTest.java new file mode 100644 index 00000000000..6f4650c13be --- /dev/null +++ b/modules/transactions/src/test/java/org/apache/ignite/internal/tx/impl/TransactionExpirationRegistryTest.java @@ -0,0 +1,167 @@ +/* + * 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. + */ + +package org.apache.ignite.internal.tx.impl; + +import static org.apache.ignite.internal.util.CompletableFutures.nullCompletedFuture; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest; +import org.apache.ignite.internal.tx.InternalTransaction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TransactionExpirationRegistryTest extends BaseIgniteAbstractTest { + private final TransactionExpirationRegistry registry = new TransactionExpirationRegistry(); + + @Mock + private InternalTransaction tx1; + + @Mock + private InternalTransaction tx2; + + @BeforeEach + void configureMocks() { + lenient().when(tx1.rollbackAsync()).thenReturn(nullCompletedFuture()); + lenient().when(tx2.rollbackAsync()).thenReturn(nullCompletedFuture()); + } + + @Test + void abortsTransactionsBeforeExpirationTime() { + registry.register(tx1, 1000); + registry.register(tx2, 2000); + + registry.expireUpTo(3000); + + verify(tx1).rollbackAsync(); + verify(tx2).rollbackAsync(); + } + + @Test + void abortsTransactionsExactlyOnExpirationTime() { + registry.register(tx1, 1000); + + registry.expireUpTo(1000); + + verify(tx1).rollbackAsync(); + } + + @Test + void doesNotAbortTransactionsAfterExpirationTime() { + registry.register(tx1, 1001); + + registry.expireUpTo(1000); + + verify(tx1, never()).rollbackAsync(); + } + + @Test + void abortsTransactionsExpiredAfterFewExpirations() { + registry.register(tx1, 1000); + + registry.expireUpTo(1000); + registry.expireUpTo(2000); + + verify(tx1).rollbackAsync(); + } + + @Test + void abortsTransactionsWithSameExpirationTime() { + registry.register(tx1, 1000); + registry.register(tx2, 1000); + + registry.expireUpTo(2000); + + verify(tx1).rollbackAsync(); + verify(tx2).rollbackAsync(); + } + + @Test + void abortsAlreadyExpiredTransactionOnRegistration() { + registry.expireUpTo(2000); + + registry.register(tx1, 1000); + registry.register(tx2, 2000); + + verify(tx1).rollbackAsync(); + verify(tx2).rollbackAsync(); + } + + @Test + void abortsAlreadyExpiredTransactionJustOnce() { + registry.expireUpTo(2000); + + registry.register(tx1, 1000); + registry.register(tx2, 2000); + + registry.expireUpTo(2000); + + verify(tx1, times(1)).rollbackAsync(); + verify(tx2, times(1)).rollbackAsync(); + } + + @Test + void abortsAllRegistered() { + registry.register(tx1, 1000); + registry.register(tx2, Long.MAX_VALUE); + + registry.abortAllRegistered(); + + verify(tx1).rollbackAsync(); + verify(tx2).rollbackAsync(); + } + + @Test + void abortsOnRegistrationAfterAbortingAllRegistered() { + registry.abortAllRegistered(); + + registry.register(tx1, 1000); + registry.register(tx2, Long.MAX_VALUE); + + verify(tx1).rollbackAsync(); + verify(tx2).rollbackAsync(); + } + + @Test + void removesTransactionOnUnregister() { + registry.register(tx1, 1000); + + registry.unregister(tx1); + + registry.expireUpTo(2000); + + // Should not be aborted due to expiration as we removed the transaction. + verify(tx1, never()).rollbackAsync(); + } + + @Test + void unregisterIsIdempotent() { + registry.register(tx1, 1000); + + registry.unregister(tx1); + + assertDoesNotThrow(() -> registry.unregister(tx1)); + } +}