Skip to content

Commit e4f28be

Browse files
authored
IGNITE-27386 Add failure reason to tx state meta (#7482)
1 parent bd006ed commit e4f28be

62 files changed

Lines changed: 1550 additions & 349 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

modules/api/src/main/java/org/apache/ignite/lang/ErrorGroups.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,9 @@ public static class Transactions {
471471

472472
/** Transaction was internally killed. This is retriable state. */
473473
public static final int TX_KILLED_ERR = TX_ERR_GROUP.registerErrorCode((short) 18);
474+
475+
/** Operation failed because the transaction is already finished due to an error. */
476+
public static final int TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR = TX_ERR_GROUP.registerErrorCode((short) 19);
474477
}
475478

476479
/** Replicator error group. */

modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static org.apache.ignite.internal.client.proto.tx.ClientTxUtils.TX_ID_DIRECT;
2323
import static org.apache.ignite.internal.client.proto.tx.ClientTxUtils.TX_ID_FIRST_DIRECT;
2424
import static org.apache.ignite.internal.hlc.HybridTimestamp.NULL_HYBRID_TIMESTAMP;
25+
import static org.apache.ignite.internal.tx.TransactionErrors.MESSAGE_TX_ALREADY_FINISHED_DUE_TO_TIMEOUT;
2526
import static org.apache.ignite.internal.util.CompletableFutures.nullCompletedFuture;
2627
import static org.apache.ignite.lang.ErrorGroups.Client.TABLE_ID_NOT_FOUND_ERR;
2728
import static org.apache.ignite.lang.ErrorGroups.Transactions.TX_ALREADY_FINISHED_WITH_TIMEOUT_ERR;
@@ -534,7 +535,7 @@ public static TableNotFoundException tableIdNotFoundException(Integer tableId) {
534535
// Remote transaction will be synchronously rolled back if the timeout has exceeded.
535536
if (remote.isRolledBackWithTimeoutExceeded()) {
536537
throw new TransactionException(TX_ALREADY_FINISHED_WITH_TIMEOUT_ERR,
537-
"Transaction is already finished [tx=" + remote + "].");
538+
MESSAGE_TX_ALREADY_FINISHED_DUE_TO_TIMEOUT + " [tx=" + remote + "].");
538539
}
539540

540541
return remote;

modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSql.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,10 @@ public <T> CompletableFuture<AsyncResultSet<T>> executeAsyncInternal(
373373
false
374374
).handle((BiFunction<AsyncResultSet<T>, Throwable, CompletableFuture<AsyncResultSet<T>>>) (r, err) -> {
375375
if (err != null) {
376+
if (tx != null && shouldRecordTransactionFailure(err)) {
377+
tx.recordOperationFailure(err);
378+
}
379+
376380
if (tx == null || !shouldTrackOperation) {
377381
return failedFuture(err);
378382
}
@@ -403,6 +407,20 @@ public <T> CompletableFuture<AsyncResultSet<T>> executeAsyncInternal(
403407
})).thenCompose(identity()).exceptionally(ClientSql::handleException);
404408
}
405409

410+
private static boolean shouldRecordTransactionFailure(Throwable err) {
411+
Throwable cause = unwrapCause(err);
412+
413+
if (!(cause instanceof SqlException)) {
414+
return true;
415+
}
416+
417+
SqlException sqlEx = (SqlException) cause;
418+
419+
return sqlEx.code() != Sql.STMT_PARSE_ERR
420+
&& sqlEx.code() != Sql.STMT_VALIDATION_ERR
421+
&& sqlEx.code() != Sql.TX_CONTROL_INSIDE_EXTERNAL_TX_ERR;
422+
}
423+
406424
private static @Nullable PartitionMapping resolveMapping(
407425
@Nullable Transaction transaction,
408426
@Nullable PartitionMappingProvider provider,

modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransaction.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import static org.apache.ignite.internal.util.ViewUtils.sync;
2929
import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR;
3030
import static org.apache.ignite.lang.ErrorGroups.Transactions.TX_ALREADY_FINISHED_ERR;
31+
import static org.apache.ignite.lang.ErrorGroups.Transactions.TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR;
32+
import static org.apache.ignite.lang.ErrorGroups.Transactions.TX_ALREADY_FINISHED_WITH_TIMEOUT_ERR;
3133
import static org.apache.ignite.lang.ErrorGroups.Transactions.TX_KILLED_ERR;
3234

3335
import java.util.ArrayList;
@@ -54,6 +56,7 @@
5456
import org.apache.ignite.internal.util.ExceptionUtils;
5557
import org.apache.ignite.internal.util.ViewUtils;
5658
import org.apache.ignite.lang.IgniteException;
59+
import org.apache.ignite.sql.SqlException;
5760
import org.apache.ignite.tx.Transaction;
5861
import org.apache.ignite.tx.TransactionException;
5962
import org.jetbrains.annotations.Nullable;
@@ -93,6 +96,12 @@ public class ClientTransaction implements Transaction {
9396
/** State. */
9497
private final AtomicInteger state = new AtomicInteger(STATE_OPEN);
9598

99+
/** The reason why the transaction became unusable before the explicit finish on the client side. */
100+
private volatile @Nullable Throwable finishCause;
101+
102+
/** Error code to expose after the transaction is finished. */
103+
private volatile int finishCode = TX_ALREADY_FINISHED_ERR;
104+
96105
/** Read-only flag. */
97106
private final boolean isReadOnly;
98107

@@ -489,6 +498,15 @@ public static ClientTransaction get(Transaction tx) {
489498
int state = clientTx.state.get();
490499

491500
if (state == STATE_OPEN) {
501+
if (clientTx.finishCause != null) {
502+
throw new TransactionException(
503+
clientTx.finishCode,
504+
format("{} [tx={}, committed=false].",
505+
finishedMessage(clientTx.finishCode),
506+
clientTx),
507+
clientTx.finishCode == TX_ALREADY_FINISHED_ERR ? null : clientTx.finishCause);
508+
}
509+
492510
return clientTx;
493511
}
494512

@@ -502,6 +520,14 @@ private static TransactionException exceptionForState(int state, ClientTransacti
502520
return new TransactionException(
503521
TX_KILLED_ERR,
504522
format("Transaction is killed [tx={}].", clientTx));
523+
} else if (clientTx.finishCode != TX_ALREADY_FINISHED_ERR) {
524+
return new TransactionException(
525+
clientTx.finishCode,
526+
format("{} [tx={}, committed={}].",
527+
finishedMessage(clientTx.finishCode),
528+
clientTx,
529+
state == STATE_COMMITTED ? "true" : "false"),
530+
clientTx.finishCause);
505531
} else {
506532
return new TransactionException(
507533
TX_ALREADY_FINISHED_ERR,
@@ -510,6 +536,56 @@ private static TransactionException exceptionForState(int state, ClientTransacti
510536
}
511537
}
512538

539+
/**
540+
* Records a failed transactional operation as the transaction finish reason.
541+
*
542+
* <p>Preserves known transaction finish codes ({@code TX_ALREADY_FINISHED_WITH_TIMEOUT_ERR},
543+
* {@code TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR}, {@code TX_KILLED_ERR}) from the given exception.
544+
* SQL operation failures are treated as {@code TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR}.
545+
* Any other failure is treated as {@code TX_ALREADY_FINISHED_ERR}.</p>
546+
*
547+
* @param cause Operation failure.
548+
*/
549+
public void recordOperationFailure(Throwable cause) {
550+
Throwable unwrapped = ExceptionUtils.unwrapCause(cause);
551+
552+
finishCause = unwrapped;
553+
554+
if (unwrapped instanceof TransactionException) {
555+
int code = ((TransactionException) unwrapped).code();
556+
557+
if (code == TX_ALREADY_FINISHED_WITH_TIMEOUT_ERR
558+
|| code == TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR
559+
|| code == TX_KILLED_ERR) {
560+
finishCode = code;
561+
return;
562+
}
563+
}
564+
565+
if (unwrapped instanceof SqlException) {
566+
finishCode = TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR;
567+
return;
568+
}
569+
570+
finishCode = TX_ALREADY_FINISHED_ERR;
571+
}
572+
573+
private static String finishedMessage(int code) {
574+
if (code == TX_ALREADY_FINISHED_WITH_TIMEOUT_ERR) {
575+
return "Transaction is already finished due to timeout";
576+
}
577+
578+
if (code == TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR) {
579+
return "Transaction is already finished due to an error";
580+
}
581+
582+
if (code == TX_KILLED_ERR) {
583+
return "Transaction is killed";
584+
}
585+
586+
return "Transaction is already finished";
587+
}
588+
513589
static IgniteException unsupportedTxTypeException(Transaction tx) {
514590
return new IgniteException(INTERNAL_ERR, "Unsupported transaction implementation: '"
515591
+ tx.getClass()

modules/client/src/test/java/org/apache/ignite/client/fakes/FakeTxManager.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ public boolean implicit() {
173173

174174
@Override
175175
public CompletableFuture<Void> finish(
176-
boolean commit, HybridTimestamp executionTimestamp, boolean full, boolean timeoutExceeded
176+
boolean commit,
177+
HybridTimestamp executionTimestamp,
178+
boolean full,
179+
@Nullable Throwable finishReason
177180
) {
178181
return nullCompletedFuture();
179182
}
@@ -194,7 +197,7 @@ public CompletableFuture<Void> kill() {
194197
}
195198

196199
@Override
197-
public CompletableFuture<Void> rollbackTimeoutExceededAsync() {
200+
public CompletableFuture<Void> rollbackWithExceptionAsync(Throwable throwable) {
198201
return nullCompletedFuture();
199202
}
200203

@@ -215,6 +218,12 @@ public <T extends TxStateMeta> T updateTxMeta(UUID txId, Function<TxStateMeta, T
215218
return null;
216219
}
217220

221+
@Override
222+
public @Nullable <T extends TxStateMeta> T enrichTxMeta(UUID txId,
223+
Function<@Nullable TxStateMeta, TxStateMeta> updater) {
224+
return null;
225+
}
226+
218227
@Override
219228
public LockManager lockManager() {
220229
return null;
@@ -230,7 +239,7 @@ public CompletableFuture<Void> finish(
230239
HybridTimestampTracker timestampTracker,
231240
ZonePartitionId commitPartition,
232241
boolean commitIntent,
233-
boolean timeoutExceeded,
242+
@Nullable Throwable finishReason,
234243
boolean recovery,
235244
boolean noRemoteWrites,
236245
Map<ZonePartitionId, PendingTxPartitionEnlistment> enlistedGroups,
@@ -297,10 +306,14 @@ public int pending() {
297306
}
298307

299308
@Override
300-
public void finishFull(
301-
HybridTimestampTracker timestampTracker, UUID txId, HybridTimestamp ts, boolean commit, boolean timeoutExceeded
309+
public CompletableFuture<Void> finishFull(
310+
HybridTimestampTracker timestampTracker,
311+
UUID txId,
312+
HybridTimestamp ts,
313+
boolean commit,
314+
Throwable finishReason
302315
) {
303-
// No-op.
316+
return nullCompletedFuture();
304317
}
305318

306319
@Override
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.ignite.internal.tx;
19+
20+
import static org.apache.ignite.lang.ErrorGroup.extractErrorCode;
21+
import static org.apache.ignite.lang.ErrorGroups.Transactions.TX_ALREADY_FINISHED_ERR;
22+
import static org.apache.ignite.lang.ErrorGroups.Transactions.TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR;
23+
import static org.apache.ignite.lang.ErrorGroups.Transactions.TX_ALREADY_FINISHED_WITH_TIMEOUT_ERR;
24+
import static org.apache.ignite.lang.ErrorGroups.errorGroupByCode;
25+
26+
import org.apache.ignite.lang.ErrorGroup;
27+
import org.jetbrains.annotations.Nullable;
28+
29+
/**
30+
* Common transaction error messages.
31+
*/
32+
public final class TransactionErrors {
33+
/** Transaction is already finished. */
34+
public static final String MESSAGE_TX_ALREADY_FINISHED = "Transaction is already finished";
35+
36+
/** Transaction is already finished due to an error. */
37+
public static final String MESSAGE_TX_ALREADY_FINISHED_DUE_TO_ERR = "Transaction is already finished due to an error";
38+
39+
/** Transaction is already finished due to timeout. */
40+
public static final String MESSAGE_TX_ALREADY_FINISHED_DUE_TO_TIMEOUT = "Transaction is already finished due to timeout";
41+
42+
/**
43+
* Returns an error code for the "transaction already finished" family.
44+
*
45+
* @param isFinishedDueToTimeout Whether the transaction was finished due to timeout.
46+
* @param isFinishedDueToError Whether the transaction was finished due to an error.
47+
*/
48+
public static int finishedTransactionErrorCode(boolean isFinishedDueToTimeout, boolean isFinishedDueToError) {
49+
if (isFinishedDueToTimeout) {
50+
return TX_ALREADY_FINISHED_WITH_TIMEOUT_ERR;
51+
}
52+
53+
return isFinishedDueToError ? TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR : TX_ALREADY_FINISHED_ERR;
54+
}
55+
56+
/**
57+
* Returns an error message for the "transaction already finished" family.
58+
*
59+
* @param isFinishedDueToTimeout Whether the transaction was finished due to timeout.
60+
* @param isFinishedDueToError Whether the transaction was finished due to an error.
61+
*/
62+
public static String finishedTransactionErrorMessage(boolean isFinishedDueToTimeout, boolean isFinishedDueToError) {
63+
if (isFinishedDueToTimeout) {
64+
return MESSAGE_TX_ALREADY_FINISHED_DUE_TO_TIMEOUT;
65+
}
66+
67+
return isFinishedDueToError ? MESSAGE_TX_ALREADY_FINISHED_DUE_TO_ERR : MESSAGE_TX_ALREADY_FINISHED;
68+
}
69+
70+
/**
71+
* Returns an error message for the "transaction already finished" family and appends cause code when cause is absent.
72+
*
73+
* @param isFinishedDueToTimeout Whether the transaction was finished due to timeout.
74+
* @param isFinishedDueToError Whether the transaction was finished due to an error.
75+
* @param causeErrorCode Error code of the failure cause, if known.
76+
* @param causePresent Whether the failure cause is present.
77+
*/
78+
public static String finishedTransactionErrorMessage(
79+
boolean isFinishedDueToTimeout,
80+
boolean isFinishedDueToError,
81+
@Nullable Integer causeErrorCode,
82+
boolean causePresent
83+
) {
84+
String message = finishedTransactionErrorMessage(isFinishedDueToTimeout, isFinishedDueToError);
85+
86+
if (!isFinishedDueToTimeout && isFinishedDueToError && !causePresent && causeErrorCode != null) {
87+
ErrorGroup errorGroup = errorGroupByCode(causeErrorCode);
88+
89+
message += " [causeCode="
90+
+ errorGroup.errorPrefix() + '-' + errorGroup.name() + '-' + Short.toUnsignedInt(extractErrorCode(causeErrorCode))
91+
+ ']';
92+
}
93+
94+
return message;
95+
}
96+
97+
private TransactionErrors() {
98+
// No-op.
99+
}
100+
}

0 commit comments

Comments
 (0)