|
20 | 20 | import static com.google.cloud.spanner.connection.AbstractStatementParser.RUN_BATCH_STATEMENT;
|
21 | 21 |
|
22 | 22 | import com.google.api.core.ApiFuture;
|
| 23 | +import com.google.api.core.ApiFutureCallback; |
23 | 24 | import com.google.api.core.ApiFutures;
|
24 | 25 | import com.google.api.core.SettableApiFuture;
|
25 | 26 | import com.google.api.gax.longrunning.OperationFuture;
|
|
42 | 43 | import com.google.cloud.spanner.SpannerException;
|
43 | 44 | import com.google.cloud.spanner.SpannerExceptionFactory;
|
44 | 45 | import com.google.cloud.spanner.TimestampBound;
|
| 46 | +import com.google.cloud.spanner.TransactionMutationLimitExceededException; |
45 | 47 | import com.google.cloud.spanner.TransactionRunner;
|
46 |
| -import com.google.cloud.spanner.Type; |
47 | 48 | import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
|
48 | 49 | import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
|
49 |
| -import com.google.cloud.spanner.connection.ReadWriteTransaction.Builder; |
50 | 50 | import com.google.common.base.Preconditions;
|
51 | 51 | import com.google.common.collect.ImmutableList;
|
52 | 52 | import com.google.common.collect.Iterables;
|
|
56 | 56 | import io.opentelemetry.context.Scope;
|
57 | 57 | import java.time.Duration;
|
58 | 58 | import java.util.Arrays;
|
| 59 | +import java.util.UUID; |
59 | 60 | import java.util.concurrent.Callable;
|
60 | 61 | import javax.annotation.Nonnull;
|
61 | 62 |
|
@@ -219,6 +220,11 @@ public boolean supportsDirectedReads(ParsedStatement parsedStatement) {
|
219 | 220 | return parsedStatement.isQuery();
|
220 | 221 | }
|
221 | 222 |
|
| 223 | + private boolean isRetryDmlAsPartitionedDml() { |
| 224 | + return this.autocommitDmlMode |
| 225 | + == AutocommitDmlMode.TRANSACTIONAL_WITH_FALLBACK_TO_PARTITIONED_NON_ATOMIC; |
| 226 | + } |
| 227 | + |
222 | 228 | private void checkAndMarkUsed() {
|
223 | 229 | Preconditions.checkState(!used, "This single-use transaction has already been used");
|
224 | 230 | used = true;
|
@@ -434,6 +440,7 @@ public ApiFuture<Long> executeUpdateAsync(
|
434 | 440 | ApiFuture<Long> res;
|
435 | 441 | switch (autocommitDmlMode) {
|
436 | 442 | case TRANSACTIONAL:
|
| 443 | + case TRANSACTIONAL_WITH_FALLBACK_TO_PARTITIONED_NON_ATOMIC: |
437 | 444 | res =
|
438 | 445 | ApiFutures.transform(
|
439 | 446 | executeTransactionalUpdateAsync(callType, update, AnalyzeMode.NONE, options),
|
@@ -561,11 +568,89 @@ private ApiFuture<Tuple<Long, ResultSet>> executeTransactionalUpdateAsync(
|
561 | 568 | throw t;
|
562 | 569 | }
|
563 | 570 | };
|
564 |
| - return executeStatementAsync( |
565 |
| - callType, |
566 |
| - update, |
567 |
| - callable, |
568 |
| - ImmutableList.of(SpannerGrpc.getExecuteSqlMethod(), SpannerGrpc.getCommitMethod())); |
| 571 | + ApiFuture<Tuple<Long, ResultSet>> transactionalResult = |
| 572 | + executeStatementAsync( |
| 573 | + callType, |
| 574 | + update, |
| 575 | + callable, |
| 576 | + ImmutableList.of(SpannerGrpc.getExecuteSqlMethod(), SpannerGrpc.getCommitMethod())); |
| 577 | + // Retry as Partitioned DML if the statement fails due to exceeding the mutation limit if that |
| 578 | + // option has been enabled. |
| 579 | + if (isRetryDmlAsPartitionedDml()) { |
| 580 | + return addRetryUpdateAsPartitionedDmlCallback(transactionalResult, callType, update, options); |
| 581 | + } |
| 582 | + return transactionalResult; |
| 583 | + } |
| 584 | + |
| 585 | + /** |
| 586 | + * Adds a callback to the given future that retries the update statement using Partitioned DML if |
| 587 | + * the original statement fails with a {@link TransactionMutationLimitExceededException}. |
| 588 | + */ |
| 589 | + private ApiFuture<Tuple<Long, ResultSet>> addRetryUpdateAsPartitionedDmlCallback( |
| 590 | + ApiFuture<Tuple<Long, ResultSet>> transactionalResult, |
| 591 | + CallType callType, |
| 592 | + final ParsedStatement update, |
| 593 | + final UpdateOption... options) { |
| 594 | + // Catch TransactionMutationLimitExceededException and retry as Partitioned DML. All other |
| 595 | + // exceptions are just propagated. |
| 596 | + return ApiFutures.catchingAsync( |
| 597 | + transactionalResult, |
| 598 | + TransactionMutationLimitExceededException.class, |
| 599 | + mutationLimitExceededException -> { |
| 600 | + UUID executionId = UUID.randomUUID(); |
| 601 | + // Invoke the retryDmlAsPartitionedDmlStarting method for the TransactionRetryListeners |
| 602 | + // that have been registered for the connection. |
| 603 | + for (TransactionRetryListener listener : this.transactionRetryListeners) { |
| 604 | + listener.retryDmlAsPartitionedDmlStarting( |
| 605 | + executionId, update.getStatement(), mutationLimitExceededException); |
| 606 | + } |
| 607 | + // Try to execute the DML statement as Partitioned DML. |
| 608 | + ApiFuture<Tuple<Long, ResultSet>> partitionedResult = |
| 609 | + ApiFutures.transform( |
| 610 | + executePartitionedUpdateAsync(callType, update, options), |
| 611 | + lowerBoundUpdateCount -> Tuple.of(lowerBoundUpdateCount, null), |
| 612 | + MoreExecutors.directExecutor()); |
| 613 | + |
| 614 | + // Add a callback to the future that invokes the TransactionRetryListeners after the |
| 615 | + // Partitioned DML statement finished. This will invoke either the Finished or Failed |
| 616 | + // method on the listeners. |
| 617 | + ApiFutures.addCallback( |
| 618 | + partitionedResult, |
| 619 | + new ApiFutureCallback<Tuple<Long, ResultSet>>() { |
| 620 | + @Override |
| 621 | + public void onFailure(Throwable throwable) { |
| 622 | + for (TransactionRetryListener listener : |
| 623 | + SingleUseTransaction.this.transactionRetryListeners) { |
| 624 | + listener.retryDmlAsPartitionedDmlFailed( |
| 625 | + executionId, update.getStatement(), throwable); |
| 626 | + } |
| 627 | + } |
| 628 | + |
| 629 | + @Override |
| 630 | + public void onSuccess(Tuple<Long, ResultSet> result) { |
| 631 | + for (TransactionRetryListener listener : |
| 632 | + SingleUseTransaction.this.transactionRetryListeners) { |
| 633 | + listener.retryDmlAsPartitionedDmlFinished( |
| 634 | + executionId, update.getStatement(), result.x()); |
| 635 | + } |
| 636 | + } |
| 637 | + }, |
| 638 | + MoreExecutors.directExecutor()); |
| 639 | + |
| 640 | + // Catch any exception from the Partitioned DML execution and throw the original |
| 641 | + // TransactionMutationLimitExceededException instead. |
| 642 | + // The exception that is returned for the Partitioned DML statement is added to the |
| 643 | + // exception as a suppressed exception. |
| 644 | + return ApiFutures.catching( |
| 645 | + partitionedResult, |
| 646 | + Throwable.class, |
| 647 | + input -> { |
| 648 | + mutationLimitExceededException.addSuppressed(input); |
| 649 | + throw mutationLimitExceededException; |
| 650 | + }, |
| 651 | + MoreExecutors.directExecutor()); |
| 652 | + }, |
| 653 | + MoreExecutors.directExecutor()); |
569 | 654 | }
|
570 | 655 |
|
571 | 656 | private ApiFuture<ResultSet> analyzeTransactionalUpdateAsync(
|
|
0 commit comments