Skip to content

Commit

Permalink
chore: Stop using message strings to determine if an error message "s…
Browse files Browse the repository at this point in the history
…ession expired"

Fixes googleapis#10620.
  • Loading branch information
jskeet committed Jul 6, 2023
1 parent 1ad9bc2 commit d9e921d
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License").
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Cloud.Spanner.Data;
using Google.Cloud.Spanner.Data.IntegrationTests;
using Grpc.Core;
using System;
using System.Threading.Tasks;
using Xunit;

namespace Google.Cloud.Spanner.V1.Internal.IntegrationTests;

[Collection(nameof(SpannerDatabaseFixture))]
[CommonTestDiagnostics]
public class ExecuteHelperTests
{
private readonly SpannerDatabaseFixture _fixture;

public ExecuteHelperTests(SpannerDatabaseFixture fixture) =>
_fixture = fixture;

[Fact]
public Task SessionNotFound() => WithSessionPool(async pool =>
{
var session = await pool.Client.CreateSessionAsync(_fixture.DatabaseName);
await pool.Client.DeleteSessionAsync(session.SessionName);
// The session doesn't become invalid immediately after deletion.
// Wait for a minute to ensure the session is really expired.
await Task.Delay(TimeSpan.FromMinutes(1));
var request = new ExecuteSqlRequest
{
Sql = $"SELECT 1",
Session = session.Name
};
var exception = await Assert.ThrowsAsync<RpcException>(() => pool.Client.ExecuteSqlAsync(request));
Assert.True(ExecuteHelper.IsSessionExpiredError(exception));
});

// This code is separated out in case we need more tests. It's really just fluff.
private async Task WithSessionPool(Func<SessionPool, Task> action)
{
var builder = new SpannerConnectionStringBuilder(_fixture.ConnectionString);
var pool = await builder.AcquireSessionPoolAsync();
try
{
await action(pool);
}
finally
{
builder.SessionPoolManager.Release(pool);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License").
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Protobuf;
using Google.Rpc;
using Grpc.Core;
using Xunit;

namespace Google.Cloud.Spanner.V1.Internal.Tests;

public class ExecuteHelperTests
{
private static readonly ResourceInfo s_sessionResourceInfo = new ResourceInfo { ResourceType = ExecuteHelper.SessionResourceType };

[Fact]
public void IsSessionExpiredError_WrongStatusCode() =>
Assert.False(CreateException(StatusCode.Aborted, s_sessionResourceInfo.ToByteArray()).IsSessionExpiredError());

[Fact]
public void IsSessionExpiredError_NoResourceInfo() =>
Assert.False(CreateException(StatusCode.NotFound, null).IsSessionExpiredError());

[Fact]
public void IsSessionExpiredError_WrongResourceInfo() =>
Assert.False(CreateException(StatusCode.NotFound, new ResourceInfo { ResourceType = "not-a-session" }.ToByteArray()).IsSessionExpiredError());

[Fact]
public void IsSessionExpiredError_InvalidResourceInfo() =>
Assert.False(CreateException(StatusCode.NotFound, new byte[1]).IsSessionExpiredError());

[Fact]
public void IsSessionExpiredError_Valid() =>
Assert.True(CreateException(StatusCode.NotFound, s_sessionResourceInfo.ToByteArray()).IsSessionExpiredError());

private static RpcException CreateException(StatusCode code, byte[] resourceInfoData)
{
var trailers = new Metadata();
if (resourceInfoData is not null)
{
trailers.Add(ExecuteHelper.ResourceInfoMetadataKey, resourceInfoData);
}
return new RpcException(new Grpc.Core.Status(code, "Bang"), trailers);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Google.Api.Gax;
using Google.Api.Gax.Grpc;
using Google.Api.Gax.Testing;
using Google.Cloud.Spanner.V1.Internal;
using Google.Cloud.Spanner.V1.Internal.Logging;
using Google.Protobuf;
using Grpc.Core;
Expand All @@ -24,6 +25,7 @@
using System.Threading.Tasks;
using Xunit;
using static Google.Cloud.Spanner.V1.TransactionOptions.ModeOneofCase;
using ResourceInfo = Google.Rpc.ResourceInfo;

namespace Google.Cloud.Spanner.V1.Tests
{
Expand Down Expand Up @@ -116,7 +118,7 @@ public async Task DetectSessionExpiry()
// Make a request which fails due to the session not being found (because it has expired).
var request = new BeginTransactionRequest();
pool.Mock.Setup(client => client.BeginTransactionAsync(request, It.IsAny<CallSettings>()))
.ThrowsAsync(new RpcException(new Status(StatusCode.NotFound, "Session not found")))
.ThrowsAsync(CreateSessionExpiredException())
.Verifiable();
await Assert.ThrowsAsync<RpcException>(() => pooledSession.BeginTransactionAsync(request, null));
Assert.True(pooledSession.ServerExpired);
Expand Down Expand Up @@ -297,7 +299,7 @@ public async Task ReleaseToPool_SessionInvalidatedByServer()
// Make a request which fails due to the session not being found (because it has expired).
var request = new BeginTransactionRequest();
pool.Mock.Setup(client => client.BeginTransactionAsync(request, It.IsAny<CallSettings>()))
.ThrowsAsync(new RpcException(new Status(StatusCode.NotFound, "Session not found")))
.ThrowsAsync(CreateSessionExpiredException())
.Verifiable();
await Assert.ThrowsAsync<RpcException>(() => pooledSession.BeginTransactionAsync(request, null));

Expand Down Expand Up @@ -392,6 +394,10 @@ public void ReleaseToPool_ReadWriteUncommittedTransactionRolledBack()
Assert.Equal(pool.RolledBackTransaction, pooledSession.TransactionId);
}

private static RpcException CreateSessionExpiredException() =>
new RpcException(new Status(StatusCode.NotFound, "Session not found"),
new Metadata { { ExecuteHelper.ResourceInfoMetadataKey, new ResourceInfo { ResourceType = ExecuteHelper.SessionResourceType }.ToByteArray() } });

private PooledSession CreateWithTransaction(SessionPool.ISessionPool pool, TransactionOptions.ModeOneofCase mode)
{
ByteString transactionId = ByteString.CopyFromUtf8(Guid.NewGuid().ToString());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2017 Google Inc. All Rights Reserved.
// Copyright 2017 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Rpc;
using Grpc.Core;
using System.Threading.Tasks;

Expand All @@ -22,6 +23,9 @@ namespace Google.Cloud.Spanner.V1.Internal
/// </summary>
public static class ExecuteHelper
{
internal const string SessionResourceType = "type.googleapis.com/google.spanner.v1.Session";
internal const string ResourceInfoMetadataKey = "google.rpc.resourceinfo-bin";

/// <summary>
/// Waits for <paramref name="task"/> to complete, handling session expiry by marking the session appropriately.
/// </summary>
Expand Down Expand Up @@ -64,10 +68,26 @@ private static bool CheckForSessionExpiredError(this RpcException rpcException,
/// <summary>
/// Determines whether <paramref name="rpcException"/> is due to a session expiry.
/// </summary>
public static bool IsSessionExpiredError(this RpcException rpcException)
public static bool IsSessionExpiredError(this RpcException rpcException) =>
rpcException?.StatusCode == StatusCode.NotFound &&
GetResourceInfoTypeFromTrailers(rpcException) == SessionResourceType;

private static string GetResourceInfoTypeFromTrailers(RpcException exception)
{
return rpcException != null && rpcException.Status.StatusCode == StatusCode.NotFound &&
rpcException.Message.Contains("Session not found");
var entry = exception.Trailers.Get(ResourceInfoMetadataKey);
if (entry is null)
{
return null;
}
try
{
return ResourceInfo.Parser.ParseFrom(entry.ValueBytes).ResourceType;
}
// If anything goes wrong when parsing, just treat it as if the entry was absent.
catch
{
return null;
}
}
}
}

0 comments on commit d9e921d

Please sign in to comment.