Skip to content

Commit e1e9152

Browse files
authored
feat: allow session pool settings in connection url (#821)
* feat: allow session pool settings in connection url * fix: use NoCredentials in test
1 parent facda8a commit e1e9152

File tree

6 files changed

+432
-160
lines changed

6 files changed

+432
-160
lines changed

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

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

1919
import com.google.common.annotations.VisibleForTesting;
2020
import com.google.common.base.Preconditions;
21+
import java.util.Objects;
2122
import org.threeten.bp.Duration;
2223

2324
/** Options for the session pool used by {@code DatabaseClient}. */
@@ -63,6 +64,48 @@ private SessionPoolOptions(Builder builder) {
6364
this.removeInactiveSessionAfter = builder.removeInactiveSessionAfter;
6465
}
6566

67+
@Override
68+
public boolean equals(Object o) {
69+
if (!(o instanceof SessionPoolOptions)) {
70+
return false;
71+
}
72+
SessionPoolOptions other = (SessionPoolOptions) o;
73+
return Objects.equals(this.minSessions, other.minSessions)
74+
&& Objects.equals(this.maxSessions, other.maxSessions)
75+
&& Objects.equals(this.incStep, other.incStep)
76+
&& Objects.equals(this.maxIdleSessions, other.maxIdleSessions)
77+
&& Objects.equals(this.writeSessionsFraction, other.writeSessionsFraction)
78+
&& Objects.equals(this.actionOnExhaustion, other.actionOnExhaustion)
79+
&& Objects.equals(this.actionOnSessionNotFound, other.actionOnSessionNotFound)
80+
&& Objects.equals(this.actionOnSessionLeak, other.actionOnSessionLeak)
81+
&& Objects.equals(
82+
this.initialWaitForSessionTimeoutMillis, other.initialWaitForSessionTimeoutMillis)
83+
&& Objects.equals(this.loopFrequency, other.loopFrequency)
84+
&& Objects.equals(this.keepAliveIntervalMinutes, other.keepAliveIntervalMinutes)
85+
&& Objects.equals(this.removeInactiveSessionAfter, other.removeInactiveSessionAfter);
86+
}
87+
88+
@Override
89+
public int hashCode() {
90+
return Objects.hash(
91+
this.minSessions,
92+
this.maxSessions,
93+
this.incStep,
94+
this.maxIdleSessions,
95+
this.writeSessionsFraction,
96+
this.actionOnExhaustion,
97+
this.actionOnSessionNotFound,
98+
this.actionOnSessionLeak,
99+
this.initialWaitForSessionTimeoutMillis,
100+
this.loopFrequency,
101+
this.keepAliveIntervalMinutes,
102+
this.removeInactiveSessionAfter);
103+
}
104+
105+
public Builder toBuilder() {
106+
return new Builder(this);
107+
}
108+
66109
public int getMinSessions() {
67110
return minSessions;
68111
}
@@ -165,6 +208,24 @@ public static class Builder {
165208
private int keepAliveIntervalMinutes = 30;
166209
private Duration removeInactiveSessionAfter = Duration.ofMinutes(55L);
167210

211+
public Builder() {}
212+
213+
private Builder(SessionPoolOptions options) {
214+
this.minSessionsSet = true;
215+
this.minSessions = options.minSessions;
216+
this.maxSessions = options.maxSessions;
217+
this.incStep = options.incStep;
218+
this.maxIdleSessions = options.maxIdleSessions;
219+
this.writeSessionsFraction = options.writeSessionsFraction;
220+
this.actionOnExhaustion = options.actionOnExhaustion;
221+
this.initialWaitForSessionTimeoutMillis = options.initialWaitForSessionTimeoutMillis;
222+
this.actionOnSessionNotFound = options.actionOnSessionNotFound;
223+
this.actionOnSessionLeak = options.actionOnSessionLeak;
224+
this.loopFrequency = options.loopFrequency;
225+
this.keepAliveIntervalMinutes = options.keepAliveIntervalMinutes;
226+
this.removeInactiveSessionAfter = options.removeInactiveSessionAfter;
227+
}
228+
168229
/**
169230
* Minimum number of sessions that this pool will always maintain. These will be created eagerly
170231
* in parallel. Defaults to 100.

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

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ public String[] getValidValues() {
150150
static final boolean DEFAULT_RETRY_ABORTS_INTERNALLY = true;
151151
private static final String DEFAULT_CREDENTIALS = null;
152152
private static final String DEFAULT_OAUTH_TOKEN = null;
153+
private static final String DEFAULT_MIN_SESSIONS = null;
154+
private static final String DEFAULT_MAX_SESSIONS = null;
153155
private static final String DEFAULT_NUM_CHANNELS = null;
154156
private static final String DEFAULT_USER_AGENT = null;
155157
private static final String DEFAULT_OPTIMIZER_VERSION = "";
@@ -172,6 +174,10 @@ public String[] getValidValues() {
172174
* OAuth token to use for authentication. Cannot be used in combination with a credentials file.
173175
*/
174176
public static final String OAUTH_TOKEN_PROPERTY_NAME = "oauthToken";
177+
/** Name of the 'minSessions' connection property. */
178+
public static final String MIN_SESSIONS_PROPERTY_NAME = "minSessions";
179+
/** Name of the 'numChannels' connection property. */
180+
public static final String MAX_SESSIONS_PROPERTY_NAME = "maxSessions";
175181
/** Name of the 'numChannels' connection property. */
176182
public static final String NUM_CHANNELS_PROPERTY_NAME = "numChannels";
177183
/** Custom user agent string is only for other Google libraries. */
@@ -204,6 +210,12 @@ public String[] getValidValues() {
204210
ConnectionProperty.createStringProperty(
205211
OAUTH_TOKEN_PROPERTY_NAME,
206212
"A valid pre-existing OAuth token to use for authentication for this connection. Setting this property will take precedence over any value set for a credentials file."),
213+
ConnectionProperty.createStringProperty(
214+
MIN_SESSIONS_PROPERTY_NAME,
215+
"The minimum number of sessions in the backing session pool. The default is 100."),
216+
ConnectionProperty.createStringProperty(
217+
MAX_SESSIONS_PROPERTY_NAME,
218+
"The maximum number of sessions in the backing session pool. The default is 400."),
207219
ConnectionProperty.createStringProperty(
208220
NUM_CHANNELS_PROPERTY_NAME,
209221
"The number of gRPC channels to use to communicate with Cloud Spanner. The default is 4."),
@@ -327,6 +339,9 @@ private boolean isValidUri(String uri) {
327339
* true.
328340
* <li>readonly (boolean): Sets the initial readonly mode for the connection. Default is
329341
* false.
342+
* <li>minSessions (int): Sets the minimum number of sessions in the backing session pool.
343+
* <li>maxSessions (int): Sets the maximum number of sessions in the backing session pool.
344+
* <li>numChannels (int): Sets the number of gRPC channels to use for the connection.
330345
* <li>retryAbortsInternally (boolean): Sets the initial retryAbortsInternally mode for the
331346
* connection. Default is true.
332347
* <li>optimizerVersion (string): Sets the query optimizer version to use for the connection.
@@ -437,6 +452,8 @@ public static Builder newBuilder() {
437452
private final Credentials credentials;
438453
private final SessionPoolOptions sessionPoolOptions;
439454
private final Integer numChannels;
455+
private final Integer minSessions;
456+
private final Integer maxSessions;
440457
private final String userAgent;
441458
private final QueryOptions queryOptions;
442459

@@ -453,7 +470,6 @@ private ConnectionOptions(Builder builder) {
453470
this.warnings = checkValidProperties(builder.uri);
454471

455472
this.uri = builder.uri;
456-
this.sessionPoolOptions = builder.sessionPoolOptions;
457473
this.credentialsUrl =
458474
builder.credentialsUrl != null ? builder.credentialsUrl : parseCredentials(builder.uri);
459475
this.oauthToken =
@@ -492,19 +508,12 @@ private ConnectionOptions(Builder builder) {
492508
} else {
493509
this.credentials = getCredentialsService().createCredentials(this.credentialsUrl);
494510
}
495-
String numChannelsValue = parseNumChannels(builder.uri);
496-
if (numChannelsValue != null) {
497-
try {
498-
this.numChannels = Integer.valueOf(numChannelsValue);
499-
} catch (NumberFormatException e) {
500-
throw SpannerExceptionFactory.newSpannerException(
501-
ErrorCode.INVALID_ARGUMENT,
502-
"Invalid numChannels value specified: " + numChannelsValue,
503-
e);
504-
}
505-
} else {
506-
this.numChannels = null;
507-
}
511+
this.minSessions =
512+
parseIntegerProperty(MIN_SESSIONS_PROPERTY_NAME, parseMinSessions(builder.uri));
513+
this.maxSessions =
514+
parseIntegerProperty(MAX_SESSIONS_PROPERTY_NAME, parseMaxSessions(builder.uri));
515+
this.numChannels =
516+
parseIntegerProperty(NUM_CHANNELS_PROPERTY_NAME, parseNumChannels(builder.uri));
508517

509518
String projectId = matcher.group(Builder.PROJECT_GROUP);
510519
if (Builder.DEFAULT_PROJECT_ID_PLACEHOLDER.equalsIgnoreCase(projectId)) {
@@ -518,6 +527,36 @@ private ConnectionOptions(Builder builder) {
518527
this.statementExecutionInterceptors =
519528
Collections.unmodifiableList(builder.statementExecutionInterceptors);
520529
this.configurator = builder.configurator;
530+
531+
if (this.minSessions != null || this.maxSessions != null) {
532+
SessionPoolOptions.Builder sessionPoolOptionsBuilder =
533+
builder.sessionPoolOptions == null
534+
? SessionPoolOptions.newBuilder()
535+
: builder.sessionPoolOptions.toBuilder();
536+
if (this.minSessions != null) {
537+
sessionPoolOptionsBuilder.setMinSessions(this.minSessions);
538+
}
539+
if (this.maxSessions != null) {
540+
sessionPoolOptionsBuilder.setMaxSessions(this.maxSessions);
541+
}
542+
this.sessionPoolOptions = sessionPoolOptionsBuilder.build();
543+
} else {
544+
this.sessionPoolOptions = builder.sessionPoolOptions;
545+
}
546+
}
547+
548+
private static Integer parseIntegerProperty(String propertyName, String value) {
549+
if (value != null) {
550+
try {
551+
return Integer.valueOf(value);
552+
} catch (NumberFormatException e) {
553+
throw SpannerExceptionFactory.newSpannerException(
554+
ErrorCode.INVALID_ARGUMENT,
555+
String.format("Invalid %s value specified: %s", propertyName, value),
556+
e);
557+
}
558+
}
559+
return null;
521560
}
522561

523562
SpannerOptionsConfigurator getConfigurator() {
@@ -565,6 +604,18 @@ static String parseOAuthToken(String uri) {
565604
return value != null ? value : DEFAULT_OAUTH_TOKEN;
566605
}
567606

607+
@VisibleForTesting
608+
static String parseMinSessions(String uri) {
609+
String value = parseUriProperty(uri, MIN_SESSIONS_PROPERTY_NAME);
610+
return value != null ? value : DEFAULT_MIN_SESSIONS;
611+
}
612+
613+
@VisibleForTesting
614+
static String parseMaxSessions(String uri) {
615+
String value = parseUriProperty(uri, MAX_SESSIONS_PROPERTY_NAME);
616+
return value != null ? value : DEFAULT_MAX_SESSIONS;
617+
}
618+
568619
@VisibleForTesting
569620
static String parseNumChannels(String uri) {
570621
String value = parseUriProperty(uri, NUM_CHANNELS_PROPERTY_NAME);
@@ -671,6 +722,24 @@ public SessionPoolOptions getSessionPoolOptions() {
671722
return sessionPoolOptions;
672723
}
673724

725+
/**
726+
* The minimum number of sessions in the backing session pool of this connection. The session pool
727+
* is shared between all connections in the same JVM that connect to the same Cloud Spanner
728+
* database using the same connection settings.
729+
*/
730+
public Integer getMinSessions() {
731+
return minSessions;
732+
}
733+
734+
/**
735+
* The maximum number of sessions in the backing session pool of this connection. The session pool
736+
* is shared between all connections in the same JVM that connect to the same Cloud Spanner
737+
* database using the same connection settings.
738+
*/
739+
public Integer getMaxSessions() {
740+
return maxSessions;
741+
}
742+
674743
/** The number of channels to use for the connection. */
675744
public Integer getNumChannels() {
676745
return numChannels;

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,19 @@ static class SpannerPoolKey {
148148
private final boolean usePlainText;
149149
private final String userAgent;
150150

151-
private static SpannerPoolKey of(ConnectionOptions options) {
151+
@VisibleForTesting
152+
static SpannerPoolKey of(ConnectionOptions options) {
152153
return new SpannerPoolKey(options);
153154
}
154155

155156
private SpannerPoolKey(ConnectionOptions options) {
156157
this.host = options.getHost();
157158
this.projectId = options.getProjectId();
158159
this.credentialsKey = CredentialsKey.create(options);
159-
this.sessionPoolOptions = options.getSessionPoolOptions();
160+
this.sessionPoolOptions =
161+
options.getSessionPoolOptions() == null
162+
? SessionPoolOptions.newBuilder().build()
163+
: options.getSessionPoolOptions();
160164
this.numChannels = options.getNumChannels();
161165
this.usePlainText = options.isUsePlainText();
162166
this.userAgent = options.getUserAgent();

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,4 +421,28 @@ public void testLenient() {
421421
assertThat(e.getMessage()).contains("bar");
422422
}
423423
}
424+
425+
@Test
426+
public void testMinSessions() {
427+
ConnectionOptions options =
428+
ConnectionOptions.newBuilder()
429+
.setUri(
430+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?minSessions=400")
431+
.setCredentialsUrl(FILE_TEST_PATH)
432+
.build();
433+
assertThat(options.getMinSessions()).isEqualTo(400);
434+
assertThat(options.getSessionPoolOptions().getMinSessions()).isEqualTo(400);
435+
}
436+
437+
@Test
438+
public void testMaxSessions() {
439+
ConnectionOptions options =
440+
ConnectionOptions.newBuilder()
441+
.setUri(
442+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?maxSessions=4000")
443+
.setCredentialsUrl(FILE_TEST_PATH)
444+
.build();
445+
assertThat(options.getMaxSessions()).isEqualTo(4000);
446+
assertThat(options.getSessionPoolOptions().getMaxSessions()).isEqualTo(4000);
447+
}
424448
}

0 commit comments

Comments
 (0)