Skip to content

Commit ff17244

Browse files
feat: add option to wait on session pool creation (#2329)
* feat: add option to wait on session pool creation Adds option to wait for min sessions to be populated in the session pool before returning the database client back to the user. This only done during the database client creation and it is useful for benchmarking. * refactor: fix imports * fix: fix comments * fix: propagate interrupt Co-authored-by: Knut Olav Løite <[email protected]> --------- Co-authored-by: Knut Olav Løite <[email protected]>
1 parent 27ef53c commit ff17244

File tree

4 files changed

+103
-2
lines changed

4 files changed

+103
-2
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java

+32
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,30 @@ class SessionPool {
128128
ErrorCode.UNIMPLEMENTED,
129129
ErrorCode.INTERNAL);
130130

131+
/**
132+
* If the {@link SessionPoolOptions#getWaitForMinSessions()} duration is greater than zero, waits
133+
* for the creation of at least {@link SessionPoolOptions#getMinSessions()} in the pool using the
134+
* given duration. If the waiting times out, a {@link SpannerException} with the {@link
135+
* ErrorCode#DEADLINE_EXCEEDED} is thrown.
136+
*/
137+
void maybeWaitOnMinSessions() {
138+
final long timeoutNanos = options.getWaitForMinSessions().toNanos();
139+
if (timeoutNanos <= 0) {
140+
return;
141+
}
142+
143+
try {
144+
if (!waitOnMinSessionsLatch.await(timeoutNanos, TimeUnit.NANOSECONDS)) {
145+
final long timeoutMillis = options.getWaitForMinSessions().toMillis();
146+
throw SpannerExceptionFactory.newSpannerException(
147+
ErrorCode.DEADLINE_EXCEEDED,
148+
"Timed out after waiting " + timeoutMillis + "ms for session pool creation");
149+
}
150+
} catch (InterruptedException e) {
151+
throw SpannerExceptionFactory.propagateInterrupt(e);
152+
}
153+
}
154+
131155
/**
132156
* Wrapper around current time so that we can fake it in tests. TODO(user): Replace with Java 8
133157
* Clock.
@@ -1855,6 +1879,8 @@ private enum Position {
18551879

18561880
@VisibleForTesting Function<PooledSession, Void> idleSessionRemovedListener;
18571881

1882+
private final CountDownLatch waitOnMinSessionsLatch;
1883+
18581884
/**
18591885
* Create a session pool with the given options and for the given database. It will also start
18601886
* eagerly creating sessions if {@link SessionPoolOptions#getMinSessions()} is greater than 0.
@@ -1934,6 +1960,8 @@ private SessionPool(
19341960
this.clock = clock;
19351961
this.poolMaintainer = new PoolMaintainer();
19361962
this.initMetricsCollection(metricRegistry, labelValues);
1963+
this.waitOnMinSessionsLatch =
1964+
options.getMinSessions() > 0 ? new CountDownLatch(1) : new CountDownLatch(0);
19371965
}
19381966

19391967
/**
@@ -2399,13 +2427,17 @@ public void onSessionReady(SessionImpl session) {
23992427
PooledSession pooledSession = null;
24002428
boolean closeSession = false;
24012429
synchronized (lock) {
2430+
int minSessions = options.getMinSessions();
24022431
pooledSession = new PooledSession(session);
24032432
numSessionsBeingCreated--;
24042433
if (closureFuture != null) {
24052434
closeSession = true;
24062435
} else {
24072436
Preconditions.checkState(totalSessions() <= options.getMaxSessions() - 1);
24082437
allSessions.add(pooledSession);
2438+
if (allSessions.size() >= minSessions) {
2439+
waitOnMinSessionsLatch.countDown();
2440+
}
24092441
if (options.isAutoDetectDialect() && !detectDialectStarted) {
24102442
// Get the dialect of the underlying database if that has not yet been done. Note that
24112443
// this method will release the session into the pool once it is done.

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java

+28-2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public class SessionPoolOptions {
5151
private final ActionOnSessionLeak actionOnSessionLeak;
5252
private final long initialWaitForSessionTimeoutMillis;
5353
private final boolean autoDetectDialect;
54+
private final Duration waitForMinSessions;
5455

5556
private SessionPoolOptions(Builder builder) {
5657
// minSessions > maxSessions is only possible if the user has only set a value for maxSessions.
@@ -69,6 +70,7 @@ private SessionPoolOptions(Builder builder) {
6970
this.keepAliveIntervalMinutes = builder.keepAliveIntervalMinutes;
7071
this.removeInactiveSessionAfter = builder.removeInactiveSessionAfter;
7172
this.autoDetectDialect = builder.autoDetectDialect;
73+
this.waitForMinSessions = builder.waitForMinSessions;
7274
}
7375

7476
@Override
@@ -90,7 +92,8 @@ public boolean equals(Object o) {
9092
&& Objects.equals(this.loopFrequency, other.loopFrequency)
9193
&& Objects.equals(this.keepAliveIntervalMinutes, other.keepAliveIntervalMinutes)
9294
&& Objects.equals(this.removeInactiveSessionAfter, other.removeInactiveSessionAfter)
93-
&& Objects.equals(this.autoDetectDialect, other.autoDetectDialect);
95+
&& Objects.equals(this.autoDetectDialect, other.autoDetectDialect)
96+
&& Objects.equals(this.waitForMinSessions, other.waitForMinSessions);
9497
}
9598

9699
@Override
@@ -108,7 +111,8 @@ public int hashCode() {
108111
this.loopFrequency,
109112
this.keepAliveIntervalMinutes,
110113
this.removeInactiveSessionAfter,
111-
this.autoDetectDialect);
114+
this.autoDetectDialect,
115+
this.waitForMinSessions);
112116
}
113117

114118
public Builder toBuilder() {
@@ -186,6 +190,11 @@ boolean isFailOnSessionLeak() {
186190
return actionOnSessionLeak == ActionOnSessionLeak.FAIL;
187191
}
188192

193+
@VisibleForTesting
194+
Duration getWaitForMinSessions() {
195+
return waitForMinSessions;
196+
}
197+
189198
public static Builder newBuilder() {
190199
return new Builder();
191200
}
@@ -229,6 +238,7 @@ public static class Builder {
229238
private int keepAliveIntervalMinutes = 30;
230239
private Duration removeInactiveSessionAfter = Duration.ofMinutes(55L);
231240
private boolean autoDetectDialect = false;
241+
private Duration waitForMinSessions = Duration.ZERO;
232242

233243
public Builder() {}
234244

@@ -247,6 +257,7 @@ private Builder(SessionPoolOptions options) {
247257
this.keepAliveIntervalMinutes = options.keepAliveIntervalMinutes;
248258
this.removeInactiveSessionAfter = options.removeInactiveSessionAfter;
249259
this.autoDetectDialect = options.autoDetectDialect;
260+
this.waitForMinSessions = options.waitForMinSessions;
250261
}
251262

252263
/**
@@ -394,6 +405,21 @@ public Builder setWriteSessionsFraction(float writeSessionsFraction) {
394405
return this;
395406
}
396407

408+
/**
409+
* If greater than zero, waits for the session pool to have at least {@link
410+
* SessionPoolOptions#minSessions} before returning the database client to the caller. Note that
411+
* this check is only done during the session pool creation. This is usually done asynchronously
412+
* in order to provide the client back to the caller as soon as possible. We don't recommend
413+
* using this option unless you are executing benchmarks and want to guarantee the session pool
414+
* has min sessions in the pool before continuing.
415+
*
416+
* <p>Defaults to zero (initialization is done asynchronously).
417+
*/
418+
public Builder setWaitForMinSessions(Duration waitForMinSessions) {
419+
this.waitForMinSessions = waitForMinSessions;
420+
return this;
421+
}
422+
397423
/** Build a SessionPoolOption object */
398424
public SessionPoolOptions build() {
399425
validate();

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java

+1
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ public DatabaseClient getDatabaseClient(DatabaseId db) {
221221
SessionPool pool =
222222
SessionPool.createPool(
223223
getOptions(), SpannerImpl.this.getSessionClient(db), labelValues);
224+
pool.maybeWaitOnMinSessions();
224225
DatabaseClientImpl dbClient = createDatabaseClient(clientId, pool);
225226
dbClients.put(db, dbClient);
226227
return dbClient;

google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java

+42
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
import org.junit.runners.Parameterized.Parameters;
9595
import org.mockito.Mock;
9696
import org.mockito.Mockito;
97+
import org.threeten.bp.Duration;
9798

9899
/** Tests for SessionPool that mock out the underlying stub. */
99100
@RunWith(Parameterized.class)
@@ -1188,6 +1189,47 @@ public void testGetDatabaseRole() throws Exception {
11881189
assertEquals(TEST_DATABASE_ROLE, pool.getDatabaseRole());
11891190
}
11901191

1192+
@Test
1193+
public void testWaitOnMinSessionsWhenSessionsAreCreatedBeforeTimeout() {
1194+
doAnswer(
1195+
invocation ->
1196+
executor.submit(
1197+
() -> {
1198+
SessionConsumerImpl consumer =
1199+
invocation.getArgument(2, SessionConsumerImpl.class);
1200+
consumer.onSessionReady(mockSession());
1201+
}))
1202+
.when(sessionClient)
1203+
.asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class));
1204+
1205+
options =
1206+
SessionPoolOptions.newBuilder()
1207+
.setMinSessions(minSessions)
1208+
.setMaxSessions(minSessions + 1)
1209+
.setWaitForMinSessions(Duration.ofSeconds(5))
1210+
.build();
1211+
pool = createPool(new FakeClock(), new FakeMetricRegistry(), SPANNER_DEFAULT_LABEL_VALUES);
1212+
pool.maybeWaitOnMinSessions();
1213+
assertTrue(pool.getNumberOfSessionsInPool() >= minSessions);
1214+
}
1215+
1216+
@Test(expected = SpannerException.class)
1217+
public void testWaitOnMinSessionsThrowsExceptionWhenTimeoutIsReached() {
1218+
// Does not call onSessionReady, so session pool is never populated
1219+
doAnswer(invocation -> null)
1220+
.when(sessionClient)
1221+
.asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class));
1222+
1223+
options =
1224+
SessionPoolOptions.newBuilder()
1225+
.setMinSessions(minSessions + 1)
1226+
.setMaxSessions(minSessions + 1)
1227+
.setWaitForMinSessions(Duration.ofMillis(100))
1228+
.build();
1229+
pool = createPool(new FakeClock(), new FakeMetricRegistry(), SPANNER_DEFAULT_LABEL_VALUES);
1230+
pool.maybeWaitOnMinSessions();
1231+
}
1232+
11911233
private void mockKeepAlive(Session session) {
11921234
ReadContext context = mock(ReadContext.class);
11931235
ResultSet resultSet = mock(ResultSet.class);

0 commit comments

Comments
 (0)