Skip to content

Commit 85213c8

Browse files
authored
feat: make leak detection configurable for connections (#2405)
Make the leak detection and pre-creation of exceptions for session and connection leaks configurable for connections.
1 parent 8aa7a1d commit 85213c8

File tree

3 files changed

+97
-3
lines changed

3 files changed

+97
-3
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private LeakedConnectionException() {
8484
}
8585
}
8686

87-
private volatile LeakedConnectionException leakedException = new LeakedConnectionException();
87+
private volatile LeakedConnectionException leakedException;;
8888
private final SpannerPool spannerPool;
8989
private AbstractStatementParser statementParser;
9090
/**
@@ -222,6 +222,8 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
222222
/** Create a connection and register it in the SpannerPool. */
223223
ConnectionImpl(ConnectionOptions options) {
224224
Preconditions.checkNotNull(options);
225+
this.leakedException =
226+
options.isTrackConnectionLeaks() ? new LeakedConnectionException() : null;
225227
this.statementExecutor = new StatementExecutor(options.getStatementExecutionInterceptors());
226228
this.spannerPool = SpannerPool.INSTANCE;
227229
this.options = options;
@@ -251,6 +253,8 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
251253
Preconditions.checkNotNull(spannerPool);
252254
Preconditions.checkNotNull(ddlClient);
253255
Preconditions.checkNotNull(dbClient);
256+
this.leakedException =
257+
options.isTrackConnectionLeaks() ? new LeakedConnectionException() : null;
254258
this.statementExecutor = new StatementExecutor(Collections.emptyList());
255259
this.spannerPool = spannerPool;
256260
this.options = options;

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ public String[] getValidValues() {
172172
private static final RpcPriority DEFAULT_RPC_PRIORITY = null;
173173
private static final boolean DEFAULT_RETURN_COMMIT_STATS = false;
174174
private static final boolean DEFAULT_LENIENT = false;
175+
private static final boolean DEFAULT_TRACK_SESSION_LEAKS = true;
176+
private static final boolean DEFAULT_TRACK_CONNECTION_LEAKS = true;
175177

176178
private static final String PLAIN_TEXT_PROTOCOL = "http:";
177179
private static final String HOST_PROTOCOL = "https:";
@@ -218,6 +220,10 @@ public String[] getValidValues() {
218220
private static final String DIALECT_PROPERTY_NAME = "dialect";
219221
/** Name of the 'databaseRole' connection property. */
220222
public static final String DATABASE_ROLE_PROPERTY_NAME = "databaseRole";
223+
/** Name of the 'trackStackTraceOfSessionCheckout' connection property. */
224+
public static final String TRACK_SESSION_LEAKS_PROPERTY_NAME = "trackSessionLeaks";
225+
/** Name of the 'trackStackTraceOfConnectionCreation' connection property. */
226+
public static final String TRACK_CONNECTION_LEAKS_PROPERTY_NAME = "trackConnectionLeaks";
221227
/** All valid connection properties. */
222228
public static final Set<ConnectionProperty> VALID_PROPERTIES =
223229
Collections.unmodifiableSet(
@@ -287,7 +293,25 @@ public String[] getValidValues() {
287293
DIALECT_PROPERTY_NAME, "Sets the dialect to use for this connection."),
288294
ConnectionProperty.createStringProperty(
289295
DATABASE_ROLE_PROPERTY_NAME,
290-
"Sets the database role to use for this connection. The default is privileges assigned to IAM role"))));
296+
"Sets the database role to use for this connection. The default is privileges assigned to IAM role"),
297+
ConnectionProperty.createBooleanProperty(
298+
TRACK_SESSION_LEAKS_PROPERTY_NAME,
299+
"Capture the call stack of the thread that checked out a session of the session pool. This will "
300+
+ "pre-create a LeakedSessionException already when a session is checked out. This can be disabled, "
301+
+ "for example if a monitoring system logs the pre-created exception. "
302+
+ "If disabled, the LeakedSessionException will only be created when an "
303+
+ "actual session leak is detected. The stack trace of the exception will "
304+
+ "in that case not contain the call stack of when the session was checked out.",
305+
DEFAULT_TRACK_SESSION_LEAKS),
306+
ConnectionProperty.createBooleanProperty(
307+
TRACK_CONNECTION_LEAKS_PROPERTY_NAME,
308+
"Capture the call stack of the thread that created a connection. This will "
309+
+ "pre-create a LeakedConnectionException already when a connection is created. "
310+
+ "This can be disabled, for example if a monitoring system logs the pre-created exception. "
311+
+ "If disabled, the LeakedConnectionException will only be created when an "
312+
+ "actual connection leak is detected. The stack trace of the exception will "
313+
+ "in that case not contain the call stack of when the connection was created.",
314+
DEFAULT_TRACK_CONNECTION_LEAKS))));
291315

292316
private static final Set<ConnectionProperty> INTERNAL_PROPERTIES =
293317
Collections.unmodifiableSet(
@@ -544,6 +568,8 @@ public static Builder newBuilder() {
544568
private final boolean returnCommitStats;
545569
private final boolean autoConfigEmulator;
546570
private final RpcPriority rpcPriority;
571+
private final boolean trackSessionLeaks;
572+
private final boolean trackConnectionLeaks;
547573

548574
private final boolean autocommit;
549575
private final boolean readOnly;
@@ -588,6 +614,8 @@ private ConnectionOptions(Builder builder) {
588614
this.usePlainText = this.autoConfigEmulator || parseUsePlainText(this.uri);
589615
this.host = determineHost(matcher, autoConfigEmulator, usePlainText);
590616
this.rpcPriority = parseRPCPriority(this.uri);
617+
this.trackSessionLeaks = parseTrackSessionLeaks(this.uri);
618+
this.trackConnectionLeaks = parseTrackConnectionLeaks(this.uri);
591619

592620
this.instanceId = matcher.group(Builder.INSTANCE_GROUP);
593621
this.databaseName = matcher.group(Builder.DATABASE_GROUP);
@@ -641,11 +669,12 @@ private ConnectionOptions(Builder builder) {
641669
Collections.unmodifiableList(builder.statementExecutionInterceptors);
642670
this.configurator = builder.configurator;
643671

644-
if (this.minSessions != null || this.maxSessions != null) {
672+
if (this.minSessions != null || this.maxSessions != null || !this.trackSessionLeaks) {
645673
SessionPoolOptions.Builder sessionPoolOptionsBuilder =
646674
builder.sessionPoolOptions == null
647675
? SessionPoolOptions.newBuilder()
648676
: builder.sessionPoolOptions.toBuilder();
677+
sessionPoolOptionsBuilder.setTrackStackTraceOfSessionCheckout(this.trackSessionLeaks);
649678
sessionPoolOptionsBuilder.setAutoDetectDialect(true);
650679
if (this.minSessions != null) {
651680
sessionPoolOptionsBuilder.setMinSessions(this.minSessions);
@@ -838,6 +867,18 @@ static boolean parseLenient(String uri) {
838867
return value != null ? Boolean.parseBoolean(value) : DEFAULT_LENIENT;
839868
}
840869

870+
@VisibleForTesting
871+
static boolean parseTrackSessionLeaks(String uri) {
872+
String value = parseUriProperty(uri, TRACK_SESSION_LEAKS_PROPERTY_NAME);
873+
return value != null ? Boolean.parseBoolean(value) : DEFAULT_TRACK_SESSION_LEAKS;
874+
}
875+
876+
@VisibleForTesting
877+
static boolean parseTrackConnectionLeaks(String uri) {
878+
String value = parseUriProperty(uri, TRACK_CONNECTION_LEAKS_PROPERTY_NAME);
879+
return value != null ? Boolean.parseBoolean(value) : DEFAULT_TRACK_CONNECTION_LEAKS;
880+
}
881+
841882
@VisibleForTesting
842883
static RpcPriority parseRPCPriority(String uri) {
843884
String value = parseUriProperty(uri, RPC_PRIORITY_NAME);
@@ -1078,6 +1119,10 @@ RpcPriority getRPCPriority() {
10781119
return rpcPriority;
10791120
}
10801121

1122+
boolean isTrackConnectionLeaks() {
1123+
return this.trackConnectionLeaks;
1124+
}
1125+
10811126
/** Interceptors that should be executed after each statement */
10821127
List<StatementExecutionInterceptor> getStatementExecutionInterceptors() {
10831128
return statementExecutionInterceptors;

google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java

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

1919
import static com.google.common.truth.Truth.assertThat;
2020
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertFalse;
2122
import static org.junit.Assert.assertThrows;
2223
import static org.junit.Assert.assertTrue;
2324

@@ -489,6 +490,50 @@ public void testMaxSessions() {
489490
assertThat(options.getSessionPoolOptions().getMaxSessions()).isEqualTo(4000);
490491
}
491492

493+
@Test
494+
public void testTrackSessionLeaks() {
495+
ConnectionOptions options =
496+
ConnectionOptions.newBuilder()
497+
.setUri(
498+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?trackSessionLeaks=false")
499+
.setCredentialsUrl(FILE_TEST_PATH)
500+
.build();
501+
assertFalse(options.getSessionPoolOptions().isTrackStackTraceOfSessionCheckout());
502+
}
503+
504+
@Test
505+
public void testTrackSessionLeaksDefault() {
506+
ConnectionOptions options =
507+
ConnectionOptions.newBuilder()
508+
.setUri(
509+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database")
510+
.setCredentialsUrl(FILE_TEST_PATH)
511+
.build();
512+
assertTrue(options.getSessionPoolOptions().isTrackStackTraceOfSessionCheckout());
513+
}
514+
515+
@Test
516+
public void testTrackConnectionLeaks() {
517+
ConnectionOptions options =
518+
ConnectionOptions.newBuilder()
519+
.setUri(
520+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?trackConnectionLeaks=false")
521+
.setCredentialsUrl(FILE_TEST_PATH)
522+
.build();
523+
assertFalse(options.isTrackConnectionLeaks());
524+
}
525+
526+
@Test
527+
public void testTrackConnectionLeaksDefault() {
528+
ConnectionOptions options =
529+
ConnectionOptions.newBuilder()
530+
.setUri(
531+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database")
532+
.setCredentialsUrl(FILE_TEST_PATH)
533+
.build();
534+
assertTrue(options.isTrackConnectionLeaks());
535+
}
536+
492537
@Test
493538
public void testLocalConnectionError() {
494539
String uri =

0 commit comments

Comments
 (0)