Skip to content

Commit ba6a0f6

Browse files
authoredMay 3, 2024··
feat: include stack trace of checked out sessions in exception (#3092)
Include the stack trace of where a session was checked out when the session pool is exhausted. This makes it easier to find the place in application code that is responsible for having checked out sessions, and potentially having leaked these.
1 parent 789b0fb commit ba6a0f6

File tree

2 files changed

+114
-8
lines changed

2 files changed

+114
-8
lines changed
 

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

+58-8
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@
8787
import io.opentelemetry.api.common.Attributes;
8888
import io.opentelemetry.api.common.AttributesBuilder;
8989
import io.opentelemetry.api.metrics.Meter;
90+
import java.io.PrintWriter;
91+
import java.io.StringWriter;
9092
import java.util.ArrayList;
9193
import java.util.HashSet;
9294
import java.util.Iterator;
@@ -577,6 +579,7 @@ public PooledSessionFuture replaceSession(
577579
numSessionsInUse--;
578580
numSessionsReleased++;
579581
checkedOutSessions.remove(session);
582+
markedCheckedOutSessions.remove(session);
580583
}
581584
session.leakedException = null;
582585
invalidateSession(session.get());
@@ -1333,6 +1336,9 @@ void clearLeakedException() {
13331336
private void markCheckedOut() {
13341337
if (options.isTrackStackTraceOfSessionCheckout()) {
13351338
this.leakedException = new LeakedSessionException();
1339+
synchronized (SessionPool.this.lock) {
1340+
SessionPool.this.markedCheckedOutSessions.add(this);
1341+
}
13361342
}
13371343
}
13381344

@@ -1526,6 +1532,7 @@ public ApiFuture<Empty> asyncClose() {
15261532
synchronized (lock) {
15271533
leakedException = null;
15281534
checkedOutSessions.remove(this);
1535+
markedCheckedOutSessions.remove(this);
15291536
}
15301537
}
15311538
return ApiFutures.immediateFuture(Empty.getDefaultInstance());
@@ -2347,7 +2354,8 @@ private PooledSession pollUninterruptiblyWithTimeout(
23472354
"Timed out after waiting "
23482355
+ acquireSessionTimeout.toMillis()
23492356
+ "ms for acquiring session. To mitigate error SessionPoolOptions#setAcquireSessionTimeout(Duration) to set a higher timeout"
2350-
+ " or increase the number of sessions in the session pool.");
2357+
+ " or increase the number of sessions in the session pool.\n"
2358+
+ createCheckedOutSessionsStackTraces());
23512359
if (waiter.setException(exception)) {
23522360
// Only throw the exception if setting it on the waiter was successful. The
23532361
// waiter.setException(..) method returns false if some other thread in the meantime
@@ -2794,6 +2802,9 @@ enum Position {
27942802
@VisibleForTesting
27952803
final Set<PooledSessionFuture> checkedOutSessions = new HashSet<>();
27962804

2805+
@GuardedBy("lock")
2806+
private final Set<PooledSessionFuture> markedCheckedOutSessions = new HashSet<>();
2807+
27972808
private final SessionConsumer sessionConsumer = new SessionConsumerImpl();
27982809

27992810
private final MultiplexedSessionInitializationConsumer multiplexedSessionInitializationConsumer =
@@ -3012,6 +3023,13 @@ int getNumberOfSessionsInUse() {
30123023
}
30133024
}
30143025

3026+
@VisibleForTesting
3027+
int getMaxSessionsInUse() {
3028+
synchronized (lock) {
3029+
return maxSessionsInUse;
3030+
}
3031+
}
3032+
30153033
@VisibleForTesting
30163034
double getRatioOfSessionsInUse() {
30173035
synchronized (lock) {
@@ -3266,22 +3284,54 @@ private void incrementNumSessionsInUse(boolean isMultiplexed) {
32663284

32673285
private void maybeCreateSession() {
32683286
ISpan span = tracer.getCurrentSpan();
3287+
boolean throwResourceExhaustedException = false;
32693288
synchronized (lock) {
32703289
if (numWaiters() >= numSessionsBeingCreated) {
32713290
if (canCreateSession()) {
32723291
span.addAnnotation("Creating sessions");
32733292
createSessions(getAllowedCreateSessions(options.getIncStep()), false);
32743293
} else if (options.isFailIfPoolExhausted()) {
3275-
span.addAnnotation("Pool exhausted. Failing");
3276-
// throw specific exception
3277-
throw newSpannerException(
3278-
ErrorCode.RESOURCE_EXHAUSTED,
3279-
"No session available in the pool. Maximum number of sessions in the pool can be"
3280-
+ " overridden by invoking SessionPoolOptions#Builder#setMaxSessions. Client can be made to block"
3281-
+ " rather than fail by setting SessionPoolOptions#Builder#setBlockIfPoolExhausted.");
3294+
throwResourceExhaustedException = true;
3295+
}
3296+
}
3297+
}
3298+
if (!throwResourceExhaustedException) {
3299+
return;
3300+
}
3301+
span.addAnnotation("Pool exhausted. Failing");
3302+
3303+
String message =
3304+
"No session available in the pool. Maximum number of sessions in the pool can be"
3305+
+ " overridden by invoking SessionPoolOptions#Builder#setMaxSessions. Client can be made to block"
3306+
+ " rather than fail by setting SessionPoolOptions#Builder#setBlockIfPoolExhausted.\n"
3307+
+ createCheckedOutSessionsStackTraces();
3308+
throw newSpannerException(ErrorCode.RESOURCE_EXHAUSTED, message);
3309+
}
3310+
3311+
private StringBuilder createCheckedOutSessionsStackTraces() {
3312+
List<PooledSessionFuture> currentlyCheckedOutSessions;
3313+
synchronized (lock) {
3314+
currentlyCheckedOutSessions = new ArrayList<>(this.markedCheckedOutSessions);
3315+
}
3316+
3317+
// Create the error message without holding the lock, as we are potentially looping through a
3318+
// large set, and analyzing a large number of stack traces.
3319+
StringBuilder stackTraces =
3320+
new StringBuilder(
3321+
"There are currently "
3322+
+ currentlyCheckedOutSessions.size()
3323+
+ " sessions checked out:\n\n");
3324+
if (options.isTrackStackTraceOfSessionCheckout()) {
3325+
for (PooledSessionFuture session : currentlyCheckedOutSessions) {
3326+
if (session.leakedException != null) {
3327+
StringWriter writer = new StringWriter();
3328+
PrintWriter printWriter = new PrintWriter(writer);
3329+
session.leakedException.printStackTrace(printWriter);
3330+
stackTraces.append(writer).append("\n\n");
32823331
}
32833332
}
32843333
}
3334+
return stackTraces;
32853335
}
32863336

32873337
private void releaseSession(Tuple<PooledSession, Integer> sessionWithPosition) {

‎google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java

+56
Original file line numberDiff line numberDiff line change
@@ -5060,6 +5060,62 @@ public void testRetryOnResourceExhausted() {
50605060
}
50615061
}
50625062

5063+
@Test
5064+
public void testSessionPoolExhaustedError_containsStackTraces() {
5065+
try (Spanner spanner =
5066+
SpannerOptions.newBuilder()
5067+
.setProjectId(TEST_PROJECT)
5068+
.setChannelProvider(channelProvider)
5069+
.setCredentials(NoCredentials.getInstance())
5070+
.setSessionPoolOption(
5071+
SessionPoolOptions.newBuilder()
5072+
.setFailIfPoolExhausted()
5073+
.setMinSessions(2)
5074+
.setMaxSessions(4)
5075+
.setWaitForMinSessions(Duration.ofSeconds(10L))
5076+
.build())
5077+
.build()
5078+
.getService()) {
5079+
DatabaseClient client =
5080+
spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
5081+
List<TransactionManager> transactions = new ArrayList<>();
5082+
// Deliberately leak 4 sessions.
5083+
for (int i = 0; i < 4; i++) {
5084+
// Get a transaction manager without doing anything with it. This will reserve a session
5085+
// from
5086+
// the pool, but not increase the number of sessions marked as in use.
5087+
transactions.add(client.transactionManager());
5088+
}
5089+
// Trying to get yet another transaction will fail.
5090+
// NOTE: This fails directly, because we have set the setFailIfPoolExhausted() option.
5091+
SpannerException spannerException =
5092+
assertThrows(SpannerException.class, client::transactionManager);
5093+
assertEquals(ErrorCode.RESOURCE_EXHAUSTED, spannerException.getErrorCode());
5094+
assertTrue(
5095+
spannerException.getMessage(),
5096+
spannerException.getMessage().contains("There are currently 4 sessions checked out:"));
5097+
assertTrue(
5098+
spannerException.getMessage(),
5099+
spannerException.getMessage().contains("Session was checked out from the pool at"));
5100+
5101+
SessionPool pool = ((DatabaseClientImpl) client).pool;
5102+
// Verify that there are no sessions in the pool.
5103+
assertEquals(0, pool.getNumberOfSessionsInPool());
5104+
// Verify that the sessions have not (yet) been marked as in use.
5105+
assertEquals(0, pool.getNumberOfSessionsInUse());
5106+
assertEquals(0, pool.getMaxSessionsInUse());
5107+
// Verify that we have 4 sessions in the pool.
5108+
assertEquals(4, pool.getTotalSessionsPlusNumSessionsBeingCreated());
5109+
5110+
// Release the sessions back into the pool.
5111+
for (TransactionManager transaction : transactions) {
5112+
transaction.close();
5113+
}
5114+
// Closing the transactions should return the sessions to the pool.
5115+
assertEquals(4, pool.getNumberOfSessionsInPool());
5116+
}
5117+
}
5118+
50635119
static void assertAsString(String expected, ResultSet resultSet, int col) {
50645120
assertEquals(expected, resultSet.getValue(col).getAsString());
50655121
assertEquals(ImmutableList.of(expected), resultSet.getValue(col).getAsStringList());

0 commit comments

Comments
 (0)
Please sign in to comment.