|
18 | 18 |
|
19 | 19 | import static com.google.common.base.Preconditions.checkState;
|
20 | 20 |
|
| 21 | +import com.google.api.gax.grpc.GrpcStatusCode; |
| 22 | +import com.google.api.gax.rpc.ServerStream; |
| 23 | +import com.google.api.gax.rpc.UnavailableException; |
21 | 24 | import com.google.cloud.spanner.SessionImpl.SessionTransaction;
|
22 | 25 | import com.google.cloud.spanner.spi.v1.SpannerRpc;
|
| 26 | +import com.google.common.base.Stopwatch; |
23 | 27 | import com.google.protobuf.ByteString;
|
24 | 28 | import com.google.spanner.v1.BeginTransactionRequest;
|
25 | 29 | import com.google.spanner.v1.ExecuteSqlRequest;
|
26 | 30 | import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
|
| 31 | +import com.google.spanner.v1.PartialResultSet; |
27 | 32 | import com.google.spanner.v1.Transaction;
|
28 | 33 | import com.google.spanner.v1.TransactionOptions;
|
29 | 34 | import com.google.spanner.v1.TransactionSelector;
|
| 35 | +import io.grpc.Status.Code; |
30 | 36 | import java.util.Map;
|
31 |
| -import java.util.concurrent.Callable; |
| 37 | +import java.util.concurrent.TimeUnit; |
| 38 | +import java.util.logging.Level; |
| 39 | +import java.util.logging.Logger; |
| 40 | +import org.threeten.bp.Duration; |
| 41 | +import org.threeten.bp.temporal.ChronoUnit; |
32 | 42 |
|
33 | 43 | /** Partitioned DML transaction for bulk updates and deletes. */
|
34 | 44 | class PartitionedDMLTransaction implements SessionTransaction {
|
| 45 | + private static final Logger log = Logger.getLogger(PartitionedDMLTransaction.class.getName()); |
| 46 | + |
35 | 47 | private final SessionImpl session;
|
36 | 48 | private final SpannerRpc rpc;
|
37 | 49 | private volatile boolean isValid = true;
|
@@ -60,41 +72,88 @@ private ByteString initTransaction() {
|
60 | 72 |
|
61 | 73 | /**
|
62 | 74 | * Executes the {@link Statement} using a partitioned dml transaction with automatic retry if the
|
63 |
| - * transaction was aborted. |
| 75 | + * transaction was aborted. The update method uses the ExecuteStreamingSql RPC to execute the |
| 76 | + * statement, and will retry the stream if an {@link UnavailableException} is thrown, using the |
| 77 | + * last seen resume token if the server returns any. |
64 | 78 | */
|
65 |
| - long executePartitionedUpdate(final Statement statement) { |
| 79 | + long executeStreamingPartitionedUpdate(final Statement statement, Duration timeout) { |
66 | 80 | checkState(isValid, "Partitioned DML has been invalidated by a new operation on the session");
|
67 |
| - Callable<com.google.spanner.v1.ResultSet> callable = |
68 |
| - new Callable<com.google.spanner.v1.ResultSet>() { |
69 |
| - @Override |
70 |
| - public com.google.spanner.v1.ResultSet call() throws Exception { |
71 |
| - ByteString transactionId = initTransaction(); |
72 |
| - final ExecuteSqlRequest.Builder builder = |
73 |
| - ExecuteSqlRequest.newBuilder() |
74 |
| - .setSql(statement.getSql()) |
75 |
| - .setQueryMode(QueryMode.NORMAL) |
76 |
| - .setSession(session.getName()) |
77 |
| - .setTransaction(TransactionSelector.newBuilder().setId(transactionId).build()); |
78 |
| - Map<String, Value> stmtParameters = statement.getParameters(); |
79 |
| - if (!stmtParameters.isEmpty()) { |
80 |
| - com.google.protobuf.Struct.Builder paramsBuilder = builder.getParamsBuilder(); |
81 |
| - for (Map.Entry<String, Value> param : stmtParameters.entrySet()) { |
82 |
| - paramsBuilder.putFields(param.getKey(), param.getValue().toProto()); |
83 |
| - builder.putParamTypes(param.getKey(), param.getValue().getType().toProto()); |
| 81 | + log.log(Level.FINER, "Starting PartitionedUpdate statement"); |
| 82 | + boolean foundStats = false; |
| 83 | + long updateCount = 0L; |
| 84 | + Duration remainingTimeout = timeout; |
| 85 | + Stopwatch stopWatch = Stopwatch.createStarted(); |
| 86 | + try { |
| 87 | + // Loop to catch AbortedExceptions. |
| 88 | + while (true) { |
| 89 | + ByteString resumeToken = ByteString.EMPTY; |
| 90 | + try { |
| 91 | + ByteString transactionId = initTransaction(); |
| 92 | + final ExecuteSqlRequest.Builder builder = |
| 93 | + ExecuteSqlRequest.newBuilder() |
| 94 | + .setSql(statement.getSql()) |
| 95 | + .setQueryMode(QueryMode.NORMAL) |
| 96 | + .setSession(session.getName()) |
| 97 | + .setTransaction(TransactionSelector.newBuilder().setId(transactionId).build()); |
| 98 | + Map<String, Value> stmtParameters = statement.getParameters(); |
| 99 | + if (!stmtParameters.isEmpty()) { |
| 100 | + com.google.protobuf.Struct.Builder paramsBuilder = builder.getParamsBuilder(); |
| 101 | + for (Map.Entry<String, Value> param : stmtParameters.entrySet()) { |
| 102 | + paramsBuilder.putFields(param.getKey(), param.getValue().toProto()); |
| 103 | + builder.putParamTypes(param.getKey(), param.getValue().getType().toProto()); |
| 104 | + } |
| 105 | + } |
| 106 | + while (true) { |
| 107 | + remainingTimeout = |
| 108 | + remainingTimeout.minus(stopWatch.elapsed(TimeUnit.MILLISECONDS), ChronoUnit.MILLIS); |
| 109 | + try { |
| 110 | + builder.setResumeToken(resumeToken); |
| 111 | + ServerStream<PartialResultSet> stream = |
| 112 | + rpc.executeStreamingPartitionedDml( |
| 113 | + builder.build(), session.getOptions(), remainingTimeout); |
| 114 | + for (PartialResultSet rs : stream) { |
| 115 | + if (rs.getResumeToken() != null && !ByteString.EMPTY.equals(rs.getResumeToken())) { |
| 116 | + resumeToken = rs.getResumeToken(); |
| 117 | + } |
| 118 | + if (rs.hasStats()) { |
| 119 | + foundStats = true; |
| 120 | + updateCount += rs.getStats().getRowCountLowerBound(); |
| 121 | + } |
| 122 | + } |
| 123 | + break; |
| 124 | + } catch (UnavailableException e) { |
| 125 | + // Retry the stream in the same transaction if the stream breaks with |
| 126 | + // UnavailableException and we have a resume token. Otherwise, we just retry the |
| 127 | + // entire transaction. |
| 128 | + if (!ByteString.EMPTY.equals(resumeToken)) { |
| 129 | + log.log( |
| 130 | + Level.FINER, |
| 131 | + "Retrying PartitionedDml stream using resume token '" |
| 132 | + + resumeToken.toStringUtf8() |
| 133 | + + "' because of broken stream", |
| 134 | + e); |
| 135 | + } else { |
| 136 | + throw new com.google.api.gax.rpc.AbortedException( |
| 137 | + e, GrpcStatusCode.of(Code.ABORTED), true); |
84 | 138 | }
|
85 | 139 | }
|
86 |
| - return rpc.executePartitionedDml(builder.build(), session.getOptions()); |
87 | 140 | }
|
88 |
| - }; |
89 |
| - com.google.spanner.v1.ResultSet resultSet = |
90 |
| - SpannerRetryHelper.runTxWithRetriesOnAborted( |
91 |
| - callable, rpc.getPartitionedDmlRetrySettings()); |
92 |
| - if (!resultSet.hasStats()) { |
93 |
| - throw new IllegalArgumentException( |
94 |
| - "Partitioned DML response missing stats possibly due to non-DML statement as input"); |
| 141 | + break; |
| 142 | + } catch (com.google.api.gax.rpc.AbortedException e) { |
| 143 | + // Retry using a new transaction but with the same session if the transaction is aborted. |
| 144 | + log.log(Level.FINER, "Retrying PartitionedDml transaction after AbortedException", e); |
| 145 | + } |
| 146 | + } |
| 147 | + if (!foundStats) { |
| 148 | + throw SpannerExceptionFactory.newSpannerException( |
| 149 | + ErrorCode.INVALID_ARGUMENT, |
| 150 | + "Partitioned DML response missing stats possibly due to non-DML statement as input"); |
| 151 | + } |
| 152 | + log.log(Level.FINER, "Finished PartitionedUpdate statement"); |
| 153 | + return updateCount; |
| 154 | + } catch (Exception e) { |
| 155 | + throw SpannerExceptionFactory.newSpannerException(e); |
95 | 156 | }
|
96 |
| - // For partitioned DML, using the row count lower bound. |
97 |
| - return resultSet.getStats().getRowCountLowerBound(); |
98 | 157 | }
|
99 | 158 |
|
100 | 159 | @Override
|
|
0 commit comments