|
21 | 21 | import static org.hamcrest.CoreMatchers.is;
|
22 | 22 | import static org.hamcrest.MatcherAssert.assertThat;
|
23 | 23 | import static org.junit.Assert.assertEquals;
|
| 24 | +import static org.junit.Assert.assertThrows; |
24 | 25 | import static org.junit.Assert.fail;
|
25 | 26 |
|
26 | 27 | import com.google.cloud.Timestamp;
|
| 28 | +import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; |
27 | 29 | import com.google.cloud.spanner.ErrorCode;
|
28 | 30 | import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
|
29 | 31 | import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
|
|
35 | 37 | import com.google.cloud.spanner.connection.it.ITTransactionRetryTest.CountTransactionRetryListener;
|
36 | 38 | import com.google.common.collect.ImmutableList;
|
37 | 39 | import com.google.protobuf.ByteString;
|
| 40 | +import com.google.protobuf.ListValue; |
| 41 | +import com.google.protobuf.Value; |
38 | 42 | import com.google.spanner.v1.CommitRequest;
|
39 | 43 | import com.google.spanner.v1.ExecuteBatchDmlRequest;
|
40 | 44 | import com.google.spanner.v1.ExecuteSqlRequest;
|
41 | 45 | import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
|
| 46 | +import com.google.spanner.v1.ResultSetMetadata; |
| 47 | +import com.google.spanner.v1.StructType; |
| 48 | +import com.google.spanner.v1.StructType.Field; |
| 49 | +import com.google.spanner.v1.Type; |
| 50 | +import com.google.spanner.v1.TypeCode; |
42 | 51 | import io.grpc.Status;
|
43 | 52 | import io.grpc.StatusRuntimeException;
|
44 | 53 | import java.util.Collections;
|
45 | 54 | import java.util.List;
|
| 55 | +import java.util.stream.Collectors; |
| 56 | +import java.util.stream.LongStream; |
46 | 57 | import org.junit.Test;
|
47 | 58 | import org.junit.runner.RunWith;
|
48 | 59 | import org.junit.runners.JUnit4;
|
@@ -497,6 +508,7 @@ public void testRetryUsesTagsWithUpdateReturning() {
|
497 | 508 | assertEquals(2L, commitRequestCount);
|
498 | 509 | }
|
499 | 510 |
|
| 511 | + @Test |
500 | 512 | public void testRetryUsesAnalyzeModeForUpdate() {
|
501 | 513 | mockSpanner.putStatementResult(
|
502 | 514 | StatementResult.query(SELECT_COUNT_STATEMENT, SELECT_COUNT_RESULTSET_BEFORE_INSERT));
|
@@ -531,6 +543,69 @@ public void testRetryUsesAnalyzeModeForUpdate() {
|
531 | 543 | assertEquals(QueryMode.NORMAL, requests.get(4).getQueryMode());
|
532 | 544 | }
|
533 | 545 |
|
| 546 | + @Test |
| 547 | + public void testAbortedWithBitReversedSequence() { |
| 548 | + // A bit-reversed sequence can only be used in a read/write transaction. However, calling |
| 549 | + // get_next_sequence_value will update the sequence durably, even if the transaction is aborted. |
| 550 | + // That means that retrying a transaction that called get_next_sequence_value will always fail. |
| 551 | + String getSequenceValuesSql = |
| 552 | + "WITH t AS (\n" |
| 553 | + + "\tselect get_next_sequence_value(sequence enhanced_sequence) AS n\n" |
| 554 | + + "\tUNION ALL\n" |
| 555 | + + "\tselect get_next_sequence_value(sequence enhanced_sequence) AS n\n" |
| 556 | + + "\tUNION ALL\n" |
| 557 | + + "\tselect get_next_sequence_value(sequence enhanced_sequence) AS n\n" |
| 558 | + + "\tUNION ALL\n" |
| 559 | + + "\tselect get_next_sequence_value(sequence enhanced_sequence) AS n\n" |
| 560 | + + "\tUNION ALL\n" |
| 561 | + + "\tselect get_next_sequence_value(sequence enhanced_sequence) AS n\n" |
| 562 | + + ")\n" |
| 563 | + + "SELECT n FROM t"; |
| 564 | + mockSpanner.putStatementResult( |
| 565 | + StatementResult.queryAndThen( |
| 566 | + Statement.of(getSequenceValuesSql), |
| 567 | + createBitReversedSequenceResultSet(1L, 5L), |
| 568 | + createBitReversedSequenceResultSet(6L, 10L))); |
| 569 | + long currentValue = 0L; |
| 570 | + try (ITConnection connection = createConnection()) { |
| 571 | + try (ResultSet resultSet = connection.executeQuery(Statement.of(getSequenceValuesSql))) { |
| 572 | + while (resultSet.next()) { |
| 573 | + assertEquals(Long.reverse(++currentValue), resultSet.getLong(0)); |
| 574 | + } |
| 575 | + } |
| 576 | + mockSpanner.abortNextStatement(); |
| 577 | + // The retry should fail, because the sequence will return new values during the retry. |
| 578 | + assertThrows(AbortedDueToConcurrentModificationException.class, connection::commit); |
| 579 | + } |
| 580 | + } |
| 581 | + |
| 582 | + static com.google.spanner.v1.ResultSet createBitReversedSequenceResultSet( |
| 583 | + long startValue, long endValue) { |
| 584 | + return com.google.spanner.v1.ResultSet.newBuilder() |
| 585 | + .setMetadata( |
| 586 | + ResultSetMetadata.newBuilder() |
| 587 | + .setRowType( |
| 588 | + StructType.newBuilder() |
| 589 | + .addFields( |
| 590 | + Field.newBuilder() |
| 591 | + .setName("n") |
| 592 | + .setType(Type.newBuilder().setCode(TypeCode.INT64).build()) |
| 593 | + .build()) |
| 594 | + .build()) |
| 595 | + .build()) |
| 596 | + .addAllRows( |
| 597 | + LongStream.range(startValue, endValue) |
| 598 | + .map(Long::reverse) |
| 599 | + .mapToObj( |
| 600 | + id -> |
| 601 | + ListValue.newBuilder() |
| 602 | + .addValues( |
| 603 | + Value.newBuilder().setStringValue(String.valueOf(id)).build()) |
| 604 | + .build()) |
| 605 | + .collect(Collectors.toList())) |
| 606 | + .build(); |
| 607 | + } |
| 608 | + |
534 | 609 | ITConnection createConnection(TransactionRetryListener listener) {
|
535 | 610 | ITConnection connection =
|
536 | 611 | super.createConnection(ImmutableList.of(), ImmutableList.of(listener));
|
|
0 commit comments