Skip to content

Commit d81da4e

Browse files
authored
chore: randomize session pool order based on TPS (#2792)
* chore: randomize session pool order based on TPS * chore: remove unnecessary changes
1 parent b782725 commit d81da4e

File tree

4 files changed

+164
-4
lines changed

4 files changed

+164
-4
lines changed

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

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,6 +1751,13 @@ final class PoolMaintainer {
17511751
*/
17521752
@VisibleForTesting Instant lastExecutionTime;
17531753

1754+
/**
1755+
* The previous numSessionsAcquired seen by the maintainer. This is used to calculate the
1756+
* transactions per second, which again is used to determine whether to randomize the order of
1757+
* the session pool.
1758+
*/
1759+
private long prevNumSessionsAcquired;
1760+
17541761
boolean closed = false;
17551762

17561763
@GuardedBy("lock")
@@ -1794,6 +1801,12 @@ void maintainPool() {
17941801
return;
17951802
}
17961803
running = true;
1804+
if (loopFrequency >= 1000L) {
1805+
SessionPool.this.transactionsPerSecond =
1806+
(SessionPool.this.numSessionsAcquired - prevNumSessionsAcquired)
1807+
/ (loopFrequency / 1000L);
1808+
}
1809+
this.prevNumSessionsAcquired = SessionPool.this.numSessionsAcquired;
17971810
}
17981811
Instant currTime = clock.instant();
17991812
removeIdleSessions(currTime);
@@ -1995,6 +2008,7 @@ enum Position {
19952008
private final SettableFuture<Dialect> dialect = SettableFuture.create();
19962009
private final String databaseRole;
19972010
private final SessionClient sessionClient;
2011+
private final int numChannels;
19982012
private final ScheduledExecutorService executor;
19992013
private final ExecutorFactory<ScheduledExecutorService> executorFactory;
20002014

@@ -2054,6 +2068,9 @@ enum Position {
20542068
@GuardedBy("lock")
20552069
private long numIdleSessionsRemoved = 0;
20562070

2071+
@GuardedBy("lock")
2072+
private long transactionsPerSecond = 0L;
2073+
20572074
@GuardedBy("lock")
20582075
private long numLeakedSessionsRemoved = 0;
20592076

@@ -2190,6 +2207,7 @@ private SessionPool(
21902207
this.executorFactory = executorFactory;
21912208
this.executor = executor;
21922209
this.sessionClient = sessionClient;
2210+
this.numChannels = sessionClient.getSpanner().getOptions().getNumChannels();
21932211
this.clock = clock;
21942212
this.initialReleasePosition = initialReleasePosition;
21952213
this.poolMaintainer = new PoolMaintainer();
@@ -2493,11 +2511,13 @@ private void releaseSession(
24932511
if (closureFuture != null) {
24942512
return;
24952513
}
2496-
if (waiters.size() == 0) {
2514+
if (waiters.isEmpty()) {
24972515
// There are no pending waiters.
2498-
// Add to a random position if the head of the session pool already contains many sessions
2499-
// with the same channel as this one.
2500-
if (session.releaseToPosition == Position.FIRST && isUnbalanced(session)) {
2516+
// Add to a random position if the transactions per second is high or the head of the
2517+
// session pool already contains many sessions with the same channel as this one.
2518+
if (session.releaseToPosition != Position.RANDOM && shouldRandomize()) {
2519+
session.releaseToPosition = Position.RANDOM;
2520+
} else if (session.releaseToPosition == Position.FIRST && isUnbalanced(session)) {
25012521
session.releaseToPosition = Position.RANDOM;
25022522
} else if (session.releaseToPosition == Position.RANDOM
25032523
&& !isNewSession
@@ -2532,6 +2552,25 @@ private void releaseSession(
25322552
}
25332553
}
25342554

2555+
/**
2556+
* Returns true if the position where we return the session should be random if:
2557+
*
2558+
* <ol>
2559+
* <li>The current TPS is higher than the configured threshold.
2560+
* <li>AND the number of sessions checked out is larger than the number of channels.
2561+
* </ol>
2562+
*
2563+
* The second check prevents the session pool from being randomized when the application is
2564+
* running many small, quick queries using a small number of parallel threads. This can cause a
2565+
* high TPS, without actually having a high degree of parallelism.
2566+
*/
2567+
@VisibleForTesting
2568+
boolean shouldRandomize() {
2569+
return this.options.getRandomizePositionQPSThreshold() > 0
2570+
&& this.transactionsPerSecond >= this.options.getRandomizePositionQPSThreshold()
2571+
&& this.numSessionsInUse >= this.numChannels;
2572+
}
2573+
25352574
private boolean isUnbalanced(PooledSession session) {
25362575
int channel = session.getChannel();
25372576
int numChannels = sessionClient.getSpanner().getOptions().getNumChannels();

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public class SessionPoolOptions {
6464
private final Duration waitForMinSessions;
6565
private final Duration acquireSessionTimeout;
6666
private final Position releaseToPosition;
67+
private final long randomizePositionQPSThreshold;
6768

6869
/** Property for allowing mocking of session maintenance clock. */
6970
private final Clock poolMaintainerClock;
@@ -89,6 +90,7 @@ private SessionPoolOptions(Builder builder) {
8990
this.waitForMinSessions = builder.waitForMinSessions;
9091
this.acquireSessionTimeout = builder.acquireSessionTimeout;
9192
this.releaseToPosition = builder.releaseToPosition;
93+
this.randomizePositionQPSThreshold = builder.randomizePositionQPSThreshold;
9294
this.inactiveTransactionRemovalOptions = builder.inactiveTransactionRemovalOptions;
9395
this.poolMaintainerClock = builder.poolMaintainerClock;
9496
}
@@ -118,6 +120,7 @@ public boolean equals(Object o) {
118120
&& Objects.equals(this.waitForMinSessions, other.waitForMinSessions)
119121
&& Objects.equals(this.acquireSessionTimeout, other.acquireSessionTimeout)
120122
&& Objects.equals(this.releaseToPosition, other.releaseToPosition)
123+
&& Objects.equals(this.randomizePositionQPSThreshold, other.randomizePositionQPSThreshold)
121124
&& Objects.equals(
122125
this.inactiveTransactionRemovalOptions, other.inactiveTransactionRemovalOptions)
123126
&& Objects.equals(this.poolMaintainerClock, other.poolMaintainerClock);
@@ -143,6 +146,7 @@ public int hashCode() {
143146
this.waitForMinSessions,
144147
this.acquireSessionTimeout,
145148
this.releaseToPosition,
149+
this.randomizePositionQPSThreshold,
146150
this.inactiveTransactionRemovalOptions,
147151
this.poolMaintainerClock);
148152
}
@@ -263,6 +267,10 @@ Position getReleaseToPosition() {
263267
return releaseToPosition;
264268
}
265269

270+
long getRandomizePositionQPSThreshold() {
271+
return randomizePositionQPSThreshold;
272+
}
273+
266274
public static Builder newBuilder() {
267275
return new Builder();
268276
}
@@ -451,6 +459,13 @@ public static class Builder {
451459
private Duration waitForMinSessions = Duration.ZERO;
452460
private Duration acquireSessionTimeout = Duration.ofSeconds(60);
453461
private Position releaseToPosition = getReleaseToPositionFromSystemProperty();
462+
/**
463+
* The session pool will randomize the position of a session that is being returned when this
464+
* threshold is exceeded. That is: If the transactions per second exceeds this threshold, then
465+
* the session pool will use a random order for the sessions instead of LIFO. The default is 0,
466+
* which means that the option is disabled.
467+
*/
468+
private long randomizePositionQPSThreshold = 0L;
454469

455470
private Clock poolMaintainerClock;
456471

@@ -487,6 +502,7 @@ private Builder(SessionPoolOptions options) {
487502
this.autoDetectDialect = options.autoDetectDialect;
488503
this.waitForMinSessions = options.waitForMinSessions;
489504
this.acquireSessionTimeout = options.acquireSessionTimeout;
505+
this.randomizePositionQPSThreshold = options.randomizePositionQPSThreshold;
490506
this.inactiveTransactionRemovalOptions = options.inactiveTransactionRemovalOptions;
491507
this.poolMaintainerClock = options.poolMaintainerClock;
492508
}
@@ -764,6 +780,13 @@ Builder setReleaseToPosition(Position releaseToPosition) {
764780
return this;
765781
}
766782

783+
Builder setRandomizePositionQPSThreshold(long randomizePositionQPSThreshold) {
784+
Preconditions.checkArgument(
785+
randomizePositionQPSThreshold >= 0L, "randomizePositionQPSThreshold must be >= 0");
786+
this.randomizePositionQPSThreshold = randomizePositionQPSThreshold;
787+
return this;
788+
}
789+
767790
/** Build a SessionPoolOption object */
768791
public SessionPoolOptions build() {
769792
validate();

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import static com.google.common.truth.Truth.assertThat;
2020
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertFalse;
22+
import static org.junit.Assert.assertTrue;
2123
import static org.mockito.Mockito.any;
2224
import static org.mockito.Mockito.doAnswer;
2325
import static org.mockito.Mockito.mock;
@@ -29,6 +31,7 @@
2931
import com.google.cloud.spanner.SessionPool.PooledSessionFuture;
3032
import com.google.cloud.spanner.SessionPool.Position;
3133
import com.google.cloud.spanner.SessionPool.SessionConsumerImpl;
34+
import com.google.common.base.Preconditions;
3235
import io.opencensus.trace.Tracing;
3336
import io.opentelemetry.api.OpenTelemetry;
3437
import java.util.ArrayList;
@@ -116,6 +119,10 @@ private SessionImpl setupMockSession(final SessionImpl session, final ReadContex
116119
}
117120

118121
private SessionPool createPool() throws Exception {
122+
return createPool(this.options);
123+
}
124+
125+
private SessionPool createPool(SessionPoolOptions options) throws Exception {
119126
// Allow sessions to be added to the head of the pool in all cases in this test, as it is
120127
// otherwise impossible to know which session exactly is getting pinged at what point in time.
121128
SessionPool pool =
@@ -324,4 +331,67 @@ public void testIdleSessions() throws Exception {
324331
}
325332
assertThat(pool.totalSessions()).isEqualTo(options.getMinSessions());
326333
}
334+
335+
@Test
336+
public void testRandomizeThreshold() throws Exception {
337+
SessionPool pool =
338+
createPool(
339+
this.options
340+
.toBuilder()
341+
.setMaxSessions(400)
342+
.setLoopFrequency(1000L)
343+
.setRandomizePositionQPSThreshold(4)
344+
.build());
345+
List<Session> sessions;
346+
347+
// Run a maintenance loop. No sessions have been checked out so far, so the TPS should be 0.
348+
runMaintenanceLoop(clock, pool, 1);
349+
assertFalse(pool.shouldRandomize());
350+
351+
// Get and return one session. This means TPS == 1.
352+
returnSessions(1, useSessions(1, pool));
353+
runMaintenanceLoop(clock, pool, 1);
354+
assertFalse(pool.shouldRandomize());
355+
356+
// Get and return four sessions. This means TPS == 4, and that no sessions are checked out.
357+
returnSessions(4, useSessions(4, pool));
358+
runMaintenanceLoop(clock, pool, 1);
359+
assertFalse(pool.shouldRandomize());
360+
361+
// Get four sessions without returning them.
362+
// This means TPS == 4 and that they are all still checked out.
363+
sessions = useSessions(4, pool);
364+
runMaintenanceLoop(clock, pool, 1);
365+
assertTrue(pool.shouldRandomize());
366+
// Returning one of the sessions reduces the number of checked out sessions enough to stop the
367+
// randomizing.
368+
returnSessions(1, sessions);
369+
runMaintenanceLoop(clock, pool, 1);
370+
assertFalse(pool.shouldRandomize());
371+
372+
// Get three more session and run the maintenance loop.
373+
// The TPS is then 3, as we've only gotten 3 sessions since the last maintenance run.
374+
// That means that we should not randomize.
375+
sessions.addAll(useSessions(3, pool));
376+
runMaintenanceLoop(clock, pool, 1);
377+
assertFalse(pool.shouldRandomize());
378+
379+
returnSessions(sessions.size(), sessions);
380+
}
381+
382+
private List<Session> useSessions(int numSessions, SessionPool pool) {
383+
List<Session> sessions = new ArrayList<>(numSessions);
384+
for (int i = 0; i < numSessions; i++) {
385+
sessions.add(pool.getSession());
386+
sessions.get(sessions.size() - 1).singleUse().executeQuery(Statement.of("SELECT 1")).next();
387+
}
388+
return sessions;
389+
}
390+
391+
private void returnSessions(int numSessions, List<Session> sessions) {
392+
Preconditions.checkArgument(numSessions <= sessions.size());
393+
for (int i = 0; i < numSessions; i++) {
394+
sessions.remove(0).close();
395+
}
396+
}
327397
}

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static org.junit.Assert.assertEquals;
2020
import static org.junit.Assert.assertFalse;
2121
import static org.junit.Assert.assertNotNull;
22+
import static org.junit.Assert.assertThrows;
2223
import static org.junit.Assert.assertTrue;
2324
import static org.junit.Assert.fail;
2425

@@ -218,4 +219,31 @@ public void verifyDefaultAcquireSessionTimeout() {
218219

219220
assertEquals(Duration.ofSeconds(60), sessionPoolOptions.getAcquireSessionTimeout());
220221
}
222+
223+
@Test
224+
public void testRandomizePositionQPSThreshold() {
225+
assertEquals(0L, SessionPoolOptions.newBuilder().build().getRandomizePositionQPSThreshold());
226+
assertEquals(
227+
4L,
228+
SessionPoolOptions.newBuilder()
229+
.setRandomizePositionQPSThreshold(4L)
230+
.build()
231+
.getRandomizePositionQPSThreshold());
232+
assertEquals(
233+
10L,
234+
SessionPoolOptions.newBuilder()
235+
.setRandomizePositionQPSThreshold(4L)
236+
.setRandomizePositionQPSThreshold(10L)
237+
.build()
238+
.getRandomizePositionQPSThreshold());
239+
assertEquals(
240+
0L,
241+
SessionPoolOptions.newBuilder()
242+
.setRandomizePositionQPSThreshold(0L)
243+
.build()
244+
.getRandomizePositionQPSThreshold());
245+
assertThrows(
246+
IllegalArgumentException.class,
247+
() -> SessionPoolOptions.newBuilder().setRandomizePositionQPSThreshold(-1L));
248+
}
221249
}

0 commit comments

Comments
 (0)