Skip to content

Commit 863e672

Browse files
committed
tests(Spanner): Explicit ManagedSession and ManagedTransaction unit tests
1 parent 521bf56 commit 863e672

File tree

3 files changed

+659
-6
lines changed

3 files changed

+659
-6
lines changed
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"):
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using Google.Api.Gax.Grpc;
16+
using Google.Api.Gax.Testing;
17+
using Google.Cloud.Spanner.Common.V1;
18+
using Google.Cloud.Spanner.V1.Internal.Logging;
19+
using Google.Protobuf.WellKnownTypes;
20+
using NSubstitute;
21+
using System;
22+
using System.Threading;
23+
using System.Threading.Tasks;
24+
using Xunit;
25+
26+
namespace Google.Cloud.Spanner.V1.Tests;
27+
28+
public class ManagedSessionTests
29+
{
30+
private static readonly DatabaseName SampleDatabaseName = new DatabaseName("project", "instance", "database");
31+
private static readonly TimeSpan SoftExpiry = TimeSpan.FromDays(7);
32+
private static readonly TimeSpan HardExpiry = TimeSpan.FromDays(27) + TimeSpan.FromHours(12);
33+
34+
[Fact]
35+
public async Task GetFreshSession_InitialCreation()
36+
{
37+
var clock = new FakeClock(new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
38+
var client = SpannerClientHelpers.CreateMockClient(Logger.DefaultLogger, clock);
39+
40+
var options = ManagedSessionOptions.Create(SampleDatabaseName, client);
41+
var managedSession = new ManagedSession(options);
42+
43+
var session1 = new Session { Name = "session1", CreateTime = clock.GetCurrentDateTimeUtc().ToTimestamp() };
44+
client.CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>())
45+
.Returns(Task.FromResult(session1));
46+
47+
await managedSession.EnsureFreshAsync(CancellationToken.None);
48+
49+
await client.Received(1).CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>());
50+
}
51+
52+
[Fact]
53+
public async Task GetFreshSession_NoExpiry()
54+
{
55+
var clock = new FakeClock(new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
56+
var client = SpannerClientHelpers.CreateMockClient(Logger.DefaultLogger, clock);
57+
58+
var options = ManagedSessionOptions.Create(SampleDatabaseName, client);
59+
var managedSession = new ManagedSession(options);
60+
61+
var session1 = new Session { Name = "session1", CreateTime = clock.GetCurrentDateTimeUtc().ToTimestamp() };
62+
client.CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>())
63+
.Returns(Task.FromResult(session1));
64+
65+
await managedSession.EnsureFreshAsync(CancellationToken.None);
66+
67+
// Advance clock a bit, but not enough for soft expiry
68+
clock.Advance(TimeSpan.FromDays(1));
69+
await managedSession.EnsureFreshAsync(CancellationToken.None);
70+
71+
// Still only 1 call to CreateSessionAsync
72+
await client.Received(1).CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>());
73+
}
74+
75+
[Fact]
76+
public async Task GetFreshSession_SoftExpiry_BackgroundRefresh()
77+
{
78+
var clock = new FakeClock(new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
79+
var client = SpannerClientHelpers.CreateMockClient(Logger.DefaultLogger, clock);
80+
81+
var options = ManagedSessionOptions.Create(SampleDatabaseName, client);
82+
var managedSession = new ManagedSession(options);
83+
84+
var session1 = new Session { Name = "session1", CreateTime = clock.GetCurrentDateTimeUtc().ToTimestamp() };
85+
var session2 = new Session { Name = "session2" }; // CreateTime will be set when it's created
86+
87+
var tcs = new TaskCompletionSource<Session>();
88+
client.CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>())
89+
.Returns(Task.FromResult(session1), tcs.Task);
90+
91+
await managedSession.EnsureFreshAsync(CancellationToken.None);
92+
93+
// Advance past soft expiry
94+
clock.Advance(SoftExpiry + TimeSpan.FromMinutes(1));
95+
session2.CreateTime = clock.GetCurrentDateTimeUtc().ToTimestamp();
96+
97+
// This should trigger background refresh but return session1 immediately
98+
await managedSession.EnsureFreshAsync(CancellationToken.None);
99+
100+
// Wait for the background task to be triggered
101+
await Task.Delay(100);
102+
103+
await client.Received(2).CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>());
104+
105+
// Complete the background refresh
106+
tcs.SetResult(session2);
107+
108+
// Yield to allow the background update to complete
109+
await Task.Yield();
110+
111+
// Next call should return session2 (indirectly verified by no more calls to CreateSessionAsync)
112+
await managedSession.EnsureFreshAsync(CancellationToken.None);
113+
await client.Received(2).CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>());
114+
}
115+
116+
[Fact]
117+
public async Task GetFreshSession_ConcurrentCalls_OnlyOneRefresh()
118+
{
119+
var clock = new FakeClock(new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
120+
var client = SpannerClientHelpers.CreateMockClient(Logger.DefaultLogger, clock);
121+
122+
var options = ManagedSessionOptions.Create(SampleDatabaseName, client);
123+
var managedSession = new ManagedSession(options);
124+
125+
var session1 = new Session { Name = "session1", CreateTime = clock.GetCurrentDateTimeUtc().ToTimestamp() };
126+
var session2 = new Session { Name = "session2" };
127+
128+
var tcs = new TaskCompletionSource<Session>();
129+
client.CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>())
130+
.Returns(Task.FromResult(session1), tcs.Task);
131+
132+
await managedSession.EnsureFreshAsync(CancellationToken.None);
133+
134+
// Advance past hard expiry
135+
clock.Advance(HardExpiry + TimeSpan.FromMinutes(1));
136+
137+
// Start multiple concurrent calls
138+
var task1 = managedSession.EnsureFreshAsync(CancellationToken.None);
139+
var task2 = managedSession.EnsureFreshAsync(CancellationToken.None);
140+
var task3 = managedSession.EnsureFreshAsync(CancellationToken.None);
141+
142+
Assert.False(task1.IsCompleted);
143+
Assert.False(task2.IsCompleted);
144+
Assert.False(task3.IsCompleted);
145+
146+
tcs.SetResult(session2);
147+
await Task.WhenAll(task1, task2, task3);
148+
149+
// Only 2 calls total (1 initial, 1 refresh)
150+
await client.Received(2).CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>());
151+
}
152+
153+
[Fact]
154+
public async Task GetFreshSession_SoftExpiry_RefreshFailure_Swallowed()
155+
{
156+
var clock = new FakeClock(new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
157+
var client = SpannerClientHelpers.CreateMockClient(Logger.DefaultLogger, clock);
158+
159+
var options = ManagedSessionOptions.Create(SampleDatabaseName, client);
160+
var managedSession = new ManagedSession(options);
161+
162+
var session1 = new Session { Name = "session1", CreateTime = clock.GetCurrentDateTimeUtc().ToTimestamp() };
163+
164+
var tcs = new TaskCompletionSource<Session>();
165+
client.CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>())
166+
.Returns(Task.FromResult(session1), tcs.Task);
167+
168+
await managedSession.EnsureFreshAsync(CancellationToken.None);
169+
170+
// Advance past soft expiry
171+
clock.Advance(SoftExpiry + TimeSpan.FromMinutes(1));
172+
173+
// Should return session1 and NOT throw
174+
await managedSession.EnsureFreshAsync(CancellationToken.None);
175+
176+
// Wait for the background task to be triggered
177+
await Task.Delay(100); // Give it some time to start
178+
179+
await client.Received(2).CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>());
180+
181+
// Complete with failure
182+
tcs.SetException(new Exception("Soft failure"));
183+
184+
// Yield to allow ContinueWith to run
185+
await Task.Yield();
186+
187+
// Still should not throw.
188+
// Note that since the previous background refresh failed, this call will trigger a NEW background refresh
189+
// because the session is still soft-expired.
190+
await managedSession.EnsureFreshAsync(CancellationToken.None);
191+
192+
// Wait for the third background task to be triggered
193+
await Task.Delay(100);
194+
await client.Received(3).CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>());
195+
}
196+
197+
[Fact]
198+
public async Task GetFreshSession_HardExpiry_BlockingRefresh()
199+
{
200+
var clock = new FakeClock(new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
201+
var client = SpannerClientHelpers.CreateMockClient(Logger.DefaultLogger, clock);
202+
203+
var options = ManagedSessionOptions.Create(SampleDatabaseName, client);
204+
var managedSession = new ManagedSession(options);
205+
206+
var session1 = new Session { Name = "session1", CreateTime = clock.GetCurrentDateTimeUtc().ToTimestamp() };
207+
var session2 = new Session { Name = "session2" };
208+
209+
var tcs = new TaskCompletionSource<Session>();
210+
client.CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>())
211+
.Returns(Task.FromResult(session1), tcs.Task);
212+
213+
await managedSession.EnsureFreshAsync(CancellationToken.None);
214+
215+
// Advance past hard expiry
216+
clock.Advance(HardExpiry + TimeSpan.FromMinutes(1));
217+
session2.CreateTime = clock.GetCurrentDateTimeUtc().ToTimestamp();
218+
219+
// This should block
220+
var refreshTask = managedSession.EnsureFreshAsync(CancellationToken.None);
221+
Assert.False(refreshTask.IsCompleted);
222+
223+
tcs.SetResult(session2);
224+
await refreshTask;
225+
226+
await client.Received(2).CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>());
227+
}
228+
229+
[Fact]
230+
public async Task GetFreshSession_HardExpiry_RefreshFailure_Propagated()
231+
{
232+
var clock = new FakeClock(new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
233+
var client = SpannerClientHelpers.CreateMockClient(Logger.DefaultLogger, clock);
234+
235+
var options = ManagedSessionOptions.Create(SampleDatabaseName, client);
236+
var managedSession = new ManagedSession(options);
237+
238+
var session1 = new Session { Name = "session1", CreateTime = clock.GetCurrentDateTimeUtc().ToTimestamp() };
239+
240+
client.CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>())
241+
.Returns(Task.FromResult(session1), Task.FromException<Session>(new Exception("Hard failure")));
242+
243+
await managedSession.EnsureFreshAsync(CancellationToken.None);
244+
245+
// Advance past hard expiry
246+
clock.Advance(HardExpiry + TimeSpan.FromMinutes(1));
247+
248+
// Should throw
249+
var ex = await Assert.ThrowsAsync<Exception>(() => managedSession.EnsureFreshAsync(CancellationToken.None));
250+
Assert.Equal("Hard failure", ex.Message);
251+
}
252+
253+
[Fact]
254+
public async Task BeginTransactionAsync_ConcurrentCalls()
255+
{
256+
var clock = new FakeClock(new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
257+
var client = SpannerClientHelpers.CreateMockClient(Logger.DefaultLogger, clock);
258+
259+
var options = ManagedSessionOptions.Create(SampleDatabaseName, client);
260+
var managedSession = new ManagedSession(options);
261+
262+
var session1 = new Session { Name = "session1", CreateTime = clock.GetCurrentDateTimeUtc().ToTimestamp() };
263+
var session2 = new Session { Name = "session2" };
264+
265+
var tcs = new TaskCompletionSource<Session>();
266+
client.CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>())
267+
.Returns(Task.FromResult(session1), tcs.Task);
268+
269+
// Initial creation
270+
await managedSession.EnsureFreshAsync(CancellationToken.None);
271+
272+
// Advance past hard expiry
273+
clock.Advance(HardExpiry + TimeSpan.FromMinutes(1));
274+
275+
// Start multiple concurrent calls to BeginTransactionAsync
276+
var task1 = managedSession.BeginTransactionAsync(new TransactionOptions(), false, false, CancellationToken.None);
277+
var task2 = managedSession.BeginTransactionAsync(new TransactionOptions(), false, false, CancellationToken.None);
278+
var task3 = managedSession.BeginTransactionAsync(new TransactionOptions(), false, false, CancellationToken.None);
279+
280+
Assert.False(task1.IsCompleted);
281+
Assert.False(task2.IsCompleted);
282+
Assert.False(task3.IsCompleted);
283+
284+
tcs.SetResult(session2);
285+
var transactions = await Task.WhenAll(task1, task2, task3);
286+
287+
// Only 2 calls total (1 initial, 1 refresh)
288+
await client.Received(2).CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>());
289+
290+
// Verify all transactions use the new session
291+
foreach (var transaction in transactions)
292+
{
293+
Assert.Equal(session2.SessionName, transaction.SessionName);
294+
}
295+
}
296+
297+
[Fact]
298+
public async Task BatchWriteAsync_EnsureFreshnessAndSetsSession()
299+
{
300+
var clock = new FakeClock(new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
301+
var client = SpannerClientHelpers.CreateMockClient(Logger.DefaultLogger, clock);
302+
303+
var options = ManagedSessionOptions.Create(SampleDatabaseName, client);
304+
var managedSession = new ManagedSession(options);
305+
306+
var session1 = new Session { Name = "session1", CreateTime = clock.GetCurrentDateTimeUtc().ToTimestamp() };
307+
client.CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>())
308+
.Returns(Task.FromResult(session1));
309+
310+
// Mock BatchWrite
311+
var batchWriteStream = Substitute.For<SpannerClient.BatchWriteStream>();
312+
client.BatchWrite(Arg.Any<BatchWriteRequest>(), Arg.Any<CallSettings>())
313+
.Returns(batchWriteStream);
314+
315+
// Execute BatchWriteAsync
316+
var request = new BatchWriteRequest();
317+
await managedSession.BatchWriteAsync(request, null);
318+
319+
// Verify session was created
320+
await client.Received(1).CreateSessionAsync(Arg.Any<CreateSessionRequest>(), Arg.Any<CallSettings>());
321+
322+
// Verify BatchWrite was called with the correct session
323+
client.Received(1).BatchWrite(Arg.Is<BatchWriteRequest>(r => r.Session == session1.Name), Arg.Any<CallSettings>());
324+
}
325+
}

0 commit comments

Comments
 (0)