public async Task ReadWriteTransaction_QueryAbortsHalfway_WithDifferentUnseenResults_CanBeRetried(bool enableInternalRetries) { string sql = $"SELECT Id FROM Foo WHERE Id IN ({_fixture.RandomLong()}, {_fixture.RandomLong()})"; // Create a result set with 2 rows. _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = V1.TypeCode.Int64 }, "Id", 1, 2)); // The following will cause the ExecuteStreamingSql method on the mock server to return an Aborted error on stream index 1 (i.e. before the row with value 2 is returned). // This simulates a transaction that is aborted while a streaming result set is still being returned to the client. var streamWritePermissions = new BlockingCollection <int> { 1 }; var executionTime = ExecutionTime.StreamException(MockSpannerService.CreateAbortedException(sql), 1, streamWritePermissions); _fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(MockSpannerService.ExecuteStreamingSql), executionTime); using var connection = CreateConnection(); await connection.OpenAsync(); using var transaction = await connection.BeginTransactionAsync(); transaction.EnableInternalRetries = enableInternalRetries; var cmd = connection.CreateSelectCommand(sql); cmd.Transaction = transaction; using var reader = await cmd.ExecuteReaderAsync(); // Only the first row of the reader. Assert.True(await reader.ReadAsync()); Assert.Equal(1, reader.GetInt64(reader.GetOrdinal("Id"))); // Now change the result of the query, but only for the second row which has not yet been // seen by this transaction. _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = V1.TypeCode.Int64 }, "Id", 1, 3)); // Try to get the second row of the result. This should succeed, even though the transaction // was aborted, retried and the reader was re-initialized under the hood. The retry succeeds // because only data that had not yet been seen by this transaction was changed. executionTime.AlwaysAllowWrite(); if (enableInternalRetries) { Assert.True(await reader.ReadAsync()); Assert.Equal(3, reader.GetInt64(reader.GetOrdinal("Id"))); // Ensure that there are no more rows in the results. Assert.False(await reader.ReadAsync()); // Check that the transaction really retried. Assert.Equal(1, transaction.RetryCount); } else { var e = await Assert.ThrowsAsync <SpannerException>(() => reader.ReadAsync()); Assert.Equal(ErrorCode.Aborted, e.ErrorCode); } }
public SpannerMockServerFixture() { SpannerMock = new MockSpannerService(); _server = new Server { Services = { Google.Cloud.Spanner.V1.Spanner.BindService(SpannerMock) }, Ports = { new ServerPort("localhost", 0, ServerCredentials.Insecure) } }; _server.Start(); }
public async Task ReadWriteTransaction_QueryAbortsHalfway_WithDifferentResults_FailsRetry() { string sql = $"SELECT Id FROM Foo WHERE Id IN ({_fixture.RandomLong()}, {_fixture.RandomLong()})"; // Create a result set with 2 rows. _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = V1.TypeCode.Int64 }, "Id", 1, 2)); // The following will cause the ExecuteStreamingSql method on the mock server to return an Aborted error on stream index 1 (i.e. before the row with value 2 is returned). // This simulates a transaction that is aborted while a streaming result set is still being returned to the client. var streamWritePermissions = new BlockingCollection <int> { 1 }; var executionTime = ExecutionTime.StreamException(MockSpannerService.CreateAbortedException(sql), 1, streamWritePermissions); _fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(MockSpannerService.ExecuteStreamingSql), executionTime); using var connection = CreateConnection(); await connection.OpenAsync(); using var transaction = await connection.BeginTransactionAsync(); var cmd = connection.CreateSelectCommand(sql); cmd.Transaction = transaction; using var reader = await cmd.ExecuteReaderAsync(); // Only the first row of the reader. Assert.True(await reader.ReadAsync()); Assert.Equal(1, reader.GetInt64(reader.GetOrdinal("Id"))); // Now change the result of the query for the record that has already been seen. _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = V1.TypeCode.Int64 }, "Id", 3, 2)); executionTime.AlwaysAllowWrite(); // Try to get the second row of the result. This will now fail. await Assert.ThrowsAsync <SpannerAbortedDueToConcurrentModificationException>(() => reader.ReadAsync()); Assert.Equal(1, transaction.RetryCount); }