Skip to content

Commit 6b6427a

Browse files
feat: capture stack trace for session checkout is now optional (#2350)
* feat: capture stack trace for session checkout is now optional The session pool by default captures the stack trace of the thread that checks out a session of the pool, so this can be used in case the session is leaked. This is done by creating an exception already at the moment that the session is checked out. Some monitoring tools log the creation of this exception, giving the impression that the application is throwing a large number of errors, while the error is actually never thrown. This commit makes this capturing optional. The default is to capture the call stack, but users can turn this off in the SessionPoolOptions. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * docs: improve javadoc and comments * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 36eb38f commit 6b6427a

File tree

3 files changed

+122
-1
lines changed

3 files changed

+122
-1
lines changed

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

+17-1
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,10 @@ final class LeakedSessionException extends RuntimeException {
10991099
private LeakedSessionException() {
11001100
super("Session was checked out from the pool at " + clock.instant());
11011101
}
1102+
1103+
private LeakedSessionException(String message) {
1104+
super(message);
1105+
}
11021106
}
11031107

11041108
private enum SessionState {
@@ -1131,7 +1135,9 @@ void clearLeakedException() {
11311135
}
11321136

11331137
private void markCheckedOut() {
1134-
this.leakedException = new LeakedSessionException();
1138+
if (options.isTrackStackTraceOfSessionCheckout()) {
1139+
this.leakedException = new LeakedSessionException();
1140+
}
11351141
}
11361142

11371143
@Override
@@ -2324,6 +2330,16 @@ ListenableFuture<Void> closeAsync(ClosedException closedException) {
23242330
} else {
23252331
logger.log(Level.WARNING, "Leaked session", session.leakedException);
23262332
}
2333+
} else {
2334+
String message =
2335+
"Leaked session. "
2336+
+ "Call SessionOptions.Builder#setTrackStackTraceOfSessionCheckout(true) to start "
2337+
+ "tracking the call stack trace of the thread that checked out the session.";
2338+
if (options.isFailOnSessionLeak()) {
2339+
throw new LeakedSessionException(message);
2340+
} else {
2341+
logger.log(Level.WARNING, message);
2342+
}
23272343
}
23282344
}
23292345
for (final PooledSession session : ImmutableList.copyOf(allSessions)) {

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

+36
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public class SessionPoolOptions {
4949
private final Duration removeInactiveSessionAfter;
5050
private final ActionOnSessionNotFound actionOnSessionNotFound;
5151
private final ActionOnSessionLeak actionOnSessionLeak;
52+
private final boolean trackStackTraceOfSessionCheckout;
5253
private final long initialWaitForSessionTimeoutMillis;
5354
private final boolean autoDetectDialect;
5455
private final Duration waitForMinSessions;
@@ -65,6 +66,7 @@ private SessionPoolOptions(Builder builder) {
6566
this.actionOnExhaustion = builder.actionOnExhaustion;
6667
this.actionOnSessionNotFound = builder.actionOnSessionNotFound;
6768
this.actionOnSessionLeak = builder.actionOnSessionLeak;
69+
this.trackStackTraceOfSessionCheckout = builder.trackStackTraceOfSessionCheckout;
6870
this.initialWaitForSessionTimeoutMillis = builder.initialWaitForSessionTimeoutMillis;
6971
this.loopFrequency = builder.loopFrequency;
7072
this.keepAliveIntervalMinutes = builder.keepAliveIntervalMinutes;
@@ -87,6 +89,8 @@ public boolean equals(Object o) {
8789
&& Objects.equals(this.actionOnExhaustion, other.actionOnExhaustion)
8890
&& Objects.equals(this.actionOnSessionNotFound, other.actionOnSessionNotFound)
8991
&& Objects.equals(this.actionOnSessionLeak, other.actionOnSessionLeak)
92+
&& Objects.equals(
93+
this.trackStackTraceOfSessionCheckout, other.trackStackTraceOfSessionCheckout)
9094
&& Objects.equals(
9195
this.initialWaitForSessionTimeoutMillis, other.initialWaitForSessionTimeoutMillis)
9296
&& Objects.equals(this.loopFrequency, other.loopFrequency)
@@ -107,6 +111,7 @@ public int hashCode() {
107111
this.actionOnExhaustion,
108112
this.actionOnSessionNotFound,
109113
this.actionOnSessionLeak,
114+
this.trackStackTraceOfSessionCheckout,
110115
this.initialWaitForSessionTimeoutMillis,
111116
this.loopFrequency,
112117
this.keepAliveIntervalMinutes,
@@ -190,6 +195,10 @@ boolean isFailOnSessionLeak() {
190195
return actionOnSessionLeak == ActionOnSessionLeak.FAIL;
191196
}
192197

198+
public boolean isTrackStackTraceOfSessionCheckout() {
199+
return trackStackTraceOfSessionCheckout;
200+
}
201+
193202
@VisibleForTesting
194203
Duration getWaitForMinSessions() {
195204
return waitForMinSessions;
@@ -234,6 +243,17 @@ public static class Builder {
234243
private long initialWaitForSessionTimeoutMillis = 30_000L;
235244
private ActionOnSessionNotFound actionOnSessionNotFound = ActionOnSessionNotFound.RETRY;
236245
private ActionOnSessionLeak actionOnSessionLeak = ActionOnSessionLeak.WARN;
246+
/**
247+
* Capture the call stack of the thread that checked out a session of the pool. This will
248+
* pre-create a {@link com.google.cloud.spanner.SessionPool.LeakedSessionException} already when
249+
* a session is checked out. This can be disabled by users, for example if their monitoring
250+
* systems log the pre-created exception. If disabled, the {@link
251+
* com.google.cloud.spanner.SessionPool.LeakedSessionException} will only be created when an
252+
* actual session leak is detected. The stack trace of the exception will in that case not
253+
* contain the call stack of when the session was checked out.
254+
*/
255+
private boolean trackStackTraceOfSessionCheckout = true;
256+
237257
private long loopFrequency = 10 * 1000L;
238258
private int keepAliveIntervalMinutes = 30;
239259
private Duration removeInactiveSessionAfter = Duration.ofMinutes(55L);
@@ -253,6 +273,7 @@ private Builder(SessionPoolOptions options) {
253273
this.initialWaitForSessionTimeoutMillis = options.initialWaitForSessionTimeoutMillis;
254274
this.actionOnSessionNotFound = options.actionOnSessionNotFound;
255275
this.actionOnSessionLeak = options.actionOnSessionLeak;
276+
this.trackStackTraceOfSessionCheckout = options.trackStackTraceOfSessionCheckout;
256277
this.loopFrequency = options.loopFrequency;
257278
this.keepAliveIntervalMinutes = options.keepAliveIntervalMinutes;
258279
this.removeInactiveSessionAfter = options.removeInactiveSessionAfter;
@@ -393,6 +414,21 @@ Builder setFailOnSessionLeak() {
393414
return this;
394415
}
395416

417+
/**
418+
* Sets whether the session pool should capture the call stack trace when a session is checked
419+
* out of the pool. This will internally prepare a {@link
420+
* com.google.cloud.spanner.SessionPool.LeakedSessionException} that will only be thrown if the
421+
* session is actually leaked. This makes it easier to debug session leaks, as the stack trace
422+
* of the thread that checked out the session will be available in the exception.
423+
*
424+
* <p>Some monitoring tools might log these exceptions even though they are not thrown. This
425+
* option can be used to suppress the creation and logging of these exceptions.
426+
*/
427+
public Builder setTrackStackTraceOfSessionCheckout(boolean trackStackTraceOfSessionCheckout) {
428+
this.trackStackTraceOfSessionCheckout = trackStackTraceOfSessionCheckout;
429+
return this;
430+
}
431+
396432
/**
397433
* @deprecated This configuration value is no longer in use. The session pool does not prepare
398434
* any sessions for read/write transactions. Instead, a transaction will automatically be

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

+69
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@
2525
import com.google.api.gax.grpc.testing.LocalChannelProvider;
2626
import com.google.cloud.NoCredentials;
2727
import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime;
28+
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
29+
import com.google.cloud.spanner.SessionPool.LeakedSessionException;
30+
import com.google.protobuf.ListValue;
31+
import com.google.protobuf.Value;
32+
import com.google.spanner.v1.ResultSetMetadata;
33+
import com.google.spanner.v1.StructType;
34+
import com.google.spanner.v1.StructType.Field;
35+
import com.google.spanner.v1.Type;
36+
import com.google.spanner.v1.TypeCode;
2837
import io.grpc.Server;
2938
import io.grpc.StatusRuntimeException;
3039
import io.grpc.inprocess.InProcessServerBuilder;
@@ -94,6 +103,66 @@ public void tearDown() {
94103
spanner.close();
95104
}
96105

106+
@Test
107+
public void testIgnoreLeakedSession() {
108+
for (boolean trackStackTraceofSessionCheckout : new boolean[] {true, false}) {
109+
SpannerOptions.Builder builder =
110+
SpannerOptions.newBuilder()
111+
.setProjectId("[PROJECT]")
112+
.setChannelProvider(channelProvider)
113+
.setCredentials(NoCredentials.getInstance());
114+
builder.setSessionPoolOption(
115+
SessionPoolOptions.newBuilder()
116+
.setMinSessions(0)
117+
.setMaxSessions(2)
118+
.setIncStep(1)
119+
.setFailOnSessionLeak()
120+
.setTrackStackTraceOfSessionCheckout(trackStackTraceofSessionCheckout)
121+
.build());
122+
Spanner spanner = builder.build().getService();
123+
DatabaseClient client =
124+
spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"));
125+
mockSpanner.putStatementResult(
126+
StatementResult.query(
127+
Statement.of("SELECT 1"),
128+
com.google.spanner.v1.ResultSet.newBuilder()
129+
.setMetadata(
130+
ResultSetMetadata.newBuilder()
131+
.setRowType(
132+
StructType.newBuilder()
133+
.addFields(
134+
Field.newBuilder()
135+
.setName("c")
136+
.setType(
137+
Type.newBuilder().setCode(TypeCode.INT64).build())
138+
.build())
139+
.build())
140+
.build())
141+
.addRows(
142+
ListValue.newBuilder()
143+
.addValues(Value.newBuilder().setStringValue("1").build())
144+
.build())
145+
.build()));
146+
147+
// Start a read-only transaction without closing it before closing the Spanner instance.
148+
// This will cause a session leak.
149+
ReadOnlyTransaction transaction = client.readOnlyTransaction();
150+
try (ResultSet resultSet = transaction.executeQuery(Statement.of("SELECT 1"))) {
151+
while (resultSet.next()) {
152+
// ignore
153+
}
154+
}
155+
LeakedSessionException exception = assertThrows(LeakedSessionException.class, spanner::close);
156+
// The top of the stack trace will be "markCheckedOut" if we keep track of the point where the
157+
// session was checked out, while it will be "closeAsync" if we don't. In the latter case, we
158+
// get the stack trace of the method that tries to close the Spanner instance, while in the
159+
// former the stack trace will contain the method that checked out the session.
160+
assertEquals(
161+
trackStackTraceofSessionCheckout ? "markCheckedOut" : "closeAsync",
162+
exception.getStackTrace()[0].getMethodName());
163+
}
164+
}
165+
97166
@Test
98167
public void testReadWriteTransactionExceptionOnCreateSession() {
99168
readWriteTransactionTest(

0 commit comments

Comments
 (0)