Skip to content

Commit

Permalink
feat: add endpoint connection URL property (#2969)
Browse files Browse the repository at this point in the history
Adds an 'endpoint' connection URL property for the Connection API.
This property can be used instead of adding the endpoint to the host
group part of the Connection URL, which again removes the need to
actually change the connection URL when connecting to for example
the emulator from the JDBC driver. The latter can instead just add
the endpoint to the Properties set that is given to the JDBC driver.
  • Loading branch information
olavloite committed Apr 2, 2024
1 parent 44c6a26 commit c9be29c
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ public String[] getValidValues() {
private static final String DEFAULT_MIN_SESSIONS = null;
private static final String DEFAULT_MAX_SESSIONS = null;
private static final String DEFAULT_NUM_CHANNELS = null;
static final String DEFAULT_ENDPOINT = null;
private static final String DEFAULT_CHANNEL_PROVIDER = null;
private static final String DEFAULT_DATABASE_ROLE = null;
private static final String DEFAULT_USER_AGENT = null;
Expand Down Expand Up @@ -234,6 +235,8 @@ public String[] getValidValues() {
public static final String MAX_SESSIONS_PROPERTY_NAME = "maxSessions";
/** Name of the 'numChannels' connection property. */
public static final String NUM_CHANNELS_PROPERTY_NAME = "numChannels";
/** Name of the 'endpoint' connection property. */
public static final String ENDPOINT_PROPERTY_NAME = "endpoint";
/** Name of the 'channelProvider' connection property. */
public static final String CHANNEL_PROVIDER_PROPERTY_NAME = "channelProvider";

Expand Down Expand Up @@ -332,6 +335,12 @@ private static String generateGuardedConnectionPropertyError(
ConnectionProperty.createStringProperty(
NUM_CHANNELS_PROPERTY_NAME,
"The number of gRPC channels to use to communicate with Cloud Spanner. The default is 4."),
ConnectionProperty.createStringProperty(
ENDPOINT_PROPERTY_NAME,
"The endpoint that the JDBC driver should connect to. "
+ "The default is the default Spanner production endpoint when autoConfigEmulator=false, "
+ "and the default Spanner emulator endpoint (localhost:9010) when autoConfigEmulator=true. "
+ "This property takes precedence over any host name at the start of the connection URL."),
ConnectionProperty.createStringProperty(
CHANNEL_PROVIDER_PROPERTY_NAME,
"The name of the channel provider class. The name must reference an implementation of ExternalChannelProvider. If this property is not set, the connection will use the default grpc channel provider."),
Expand Down Expand Up @@ -738,7 +747,9 @@ private ConnectionOptions(Builder builder) {
this.autoConfigEmulator = parseAutoConfigEmulator(this.uri);
this.dialect = parseDialect(this.uri);
this.usePlainText = this.autoConfigEmulator || parseUsePlainText(this.uri);
this.host = determineHost(matcher, autoConfigEmulator, usePlainText, System.getenv());
this.host =
determineHost(
matcher, parseEndpoint(this.uri), autoConfigEmulator, usePlainText, System.getenv());
this.rpcPriority = parseRPCPriority(this.uri);
this.delayTransactionStartUntilFirstWrite = parseDelayTransactionStartUntilFirstWrite(this.uri);
this.trackSessionLeaks = parseTrackSessionLeaks(this.uri);
Expand Down Expand Up @@ -829,10 +840,12 @@ private ConnectionOptions(Builder builder) {
@VisibleForTesting
static String determineHost(
Matcher matcher,
String endpoint,
boolean autoConfigEmulator,
boolean usePlainText,
Map<String, String> environment) {
if (matcher.group(Builder.HOST_GROUP) == null) {
String host;
if (Objects.equals(endpoint, DEFAULT_ENDPOINT) && matcher.group(Builder.HOST_GROUP) == null) {
if (autoConfigEmulator) {
if (Strings.isNullOrEmpty(environment.get(SPANNER_EMULATOR_HOST_ENV_VAR))) {
return DEFAULT_EMULATOR_HOST;
Expand All @@ -842,13 +855,18 @@ static String determineHost(
} else {
return DEFAULT_HOST;
}
} else if (!Objects.equals(endpoint, DEFAULT_ENDPOINT)) {
// Add '//' at the start of the endpoint to conform to the standard URL specification.
host = "//" + endpoint;
} else {
if (usePlainText) {
return PLAIN_TEXT_PROTOCOL + matcher.group(Builder.HOST_GROUP);
} else {
return HOST_PROTOCOL + matcher.group(Builder.HOST_GROUP);
}
// The leading '//' is already included in the regex for the connection URL, so we don't need
// to add the leading '//' to the host name here.
host = matcher.group(Builder.HOST_GROUP);
}
if (usePlainText) {
return PLAIN_TEXT_PROTOCOL + host;
}
return HOST_PROTOCOL + host;
}

private static Integer parseIntegerProperty(String propertyName, String value) {
Expand Down Expand Up @@ -1013,6 +1031,11 @@ static String parseNumChannels(String uri) {
return value != null ? value : DEFAULT_NUM_CHANNELS;
}

private static String parseEndpoint(String uri) {
String value = parseUriProperty(uri, ENDPOINT_PROPERTY_NAME);
return value != null ? value : DEFAULT_ENDPOINT;
}

@VisibleForTesting
static String parseChannelProvider(String uri) {
String value = parseUriProperty(uri, CHANNEL_PROVIDER_PROPERTY_NAME);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.google.cloud.spanner.connection;

import static com.google.cloud.spanner.connection.ConnectionOptions.Builder.SPANNER_URI_PATTERN;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENDPOINT;
import static com.google.cloud.spanner.connection.ConnectionOptions.determineHost;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
Expand Down Expand Up @@ -172,41 +173,47 @@ public void testDetermineHost() {
DEFAULT_HOST,
determineHost(
matcherWithoutHost,
DEFAULT_ENDPOINT,
/* autoConfigEmulator= */ false,
/* usePlainText= */ false,
ImmutableMap.of()));
assertEquals(
DEFAULT_HOST,
determineHost(
matcherWithoutHost,
DEFAULT_ENDPOINT,
/* autoConfigEmulator= */ false,
/* usePlainText= */ false,
ImmutableMap.of("FOO", "bar")));
assertEquals(
"http://localhost:9010",
determineHost(
matcherWithoutHost,
DEFAULT_ENDPOINT,
/* autoConfigEmulator= */ true,
/* usePlainText= */ false,
ImmutableMap.of()));
assertEquals(
"http://localhost:9011",
determineHost(
matcherWithoutHost,
DEFAULT_ENDPOINT,
/* autoConfigEmulator= */ true,
/* usePlainText= */ false,
ImmutableMap.of("SPANNER_EMULATOR_HOST", "localhost:9011")));
assertEquals(
"http://localhost:9010",
determineHost(
matcherWithoutHost,
DEFAULT_ENDPOINT,
/* autoConfigEmulator= */ true,
/* usePlainText= */ true,
ImmutableMap.of()));
assertEquals(
"http://localhost:9011",
determineHost(
matcherWithoutHost,
DEFAULT_ENDPOINT,
/* autoConfigEmulator= */ true,
/* usePlainText= */ true,
ImmutableMap.of("SPANNER_EMULATOR_HOST", "localhost:9011")));
Expand All @@ -216,44 +223,80 @@ public void testDetermineHost() {
"https://custom.host.domain:1234",
determineHost(
matcherWithHost,
DEFAULT_ENDPOINT,
/* autoConfigEmulator= */ false,
/* usePlainText= */ false,
ImmutableMap.of()));
assertEquals(
"http://custom.host.domain:1234",
determineHost(
matcherWithHost,
DEFAULT_ENDPOINT,
/* autoConfigEmulator= */ false,
/* usePlainText= */ true,
ImmutableMap.of()));
assertEquals(
"http://custom.host.domain:1234",
determineHost(
matcherWithHost,
DEFAULT_ENDPOINT,
/* autoConfigEmulator= */ false,
/* usePlainText= */ true,
ImmutableMap.of()));
assertEquals(
"https://custom.host.domain:1234",
determineHost(
matcherWithHost,
DEFAULT_ENDPOINT,
/* autoConfigEmulator= */ true,
/* usePlainText= */ false,
ImmutableMap.of()));
assertEquals(
"http://custom.host.domain:1234",
determineHost(
matcherWithHost,
DEFAULT_ENDPOINT,
/* autoConfigEmulator= */ false,
/* usePlainText= */ true,
ImmutableMap.of("SPANNER_EMULATOR_HOST", "localhost:9011")));
assertEquals(
"https://custom.host.domain:1234",
determineHost(
matcherWithHost,
DEFAULT_ENDPOINT,
/* autoConfigEmulator= */ true,
/* usePlainText= */ false,
ImmutableMap.of("SPANNER_EMULATOR_HOST", "localhost:9011")));

// The 'endpoint' connection URL property can also be used to connect to the emulator.
// Using this property is sometimes easier than adding the URL to the host part of the
// connection string, for example because it can be added to the Properties object that
// is used by JDBC.
assertEquals(
"http://localhost:9010",
determineHost(
matcherWithoutHost,
"localhost:9010",
/* autoConfigEmulator= */ false,
/* usePlainText= */ true,
ImmutableMap.of()));
// A value for the 'endpoint' connection property overrides any value in the host group.
assertEquals(
"https://my.endpoint:1234",
determineHost(
matcherWithHost,
"my.endpoint:1234",
/* autoConfigEmulator= */ false,
/* usePlainText= */ false,
ImmutableMap.of("SPANNER_EMULATOR_HOST", "localhost:9011")));
assertEquals(
"http://my.endpoint.local:1234",
determineHost(
matcherWithHost,
"my.endpoint.local:1234",
/* autoConfigEmulator= */ false,
/* usePlainText= */ true,
ImmutableMap.of()));
}

@Test
Expand Down Expand Up @@ -291,6 +334,20 @@ public void testBuildWithAutoConfigEmulatorAndHost() {
assertTrue(options.isUsePlainText());
}

@Test
public void testBuildWithAutoConfigEmulatorAndEndpoint() {
ConnectionOptions.Builder builder = ConnectionOptions.newBuilder();
builder.setUri(
"cloudspanner:/projects/test-project-123/instances/test-instance-123/databases/test-database-123?autoConfigEmulator=true;endpoint=central-emulator.local:8080");
ConnectionOptions options = builder.build();
assertEquals("http://central-emulator.local:8080", options.getHost());
assertEquals("test-project-123", options.getProjectId());
assertEquals("test-instance-123", options.getInstanceId());
assertEquals("test-database-123", options.getDatabaseName());
assertEquals(NoCredentials.getInstance(), options.getCredentials());
assertTrue(options.isUsePlainText());
}

@Test
public void testBuildWithDefaultProjectPlaceholder() {
ConnectionOptions.Builder builder = ConnectionOptions.newBuilder();
Expand Down

0 comments on commit c9be29c

Please sign in to comment.