Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: improve error messages #1011

Merged
merged 4 commits into from Mar 28, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -17,6 +17,10 @@
package com.google.cloud.spanner.connection;

import com.google.api.core.InternalApi;
import com.google.api.gax.core.NoCredentialsProvider;
import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
import com.google.api.gax.rpc.UnavailableException;
import com.google.api.gax.rpc.UnimplementedException;
import com.google.auth.Credentials;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
Expand All @@ -30,10 +34,14 @@
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.admin.instance.v1.stub.GrpcInstanceAdminStub;
import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.spanner.admin.instance.v1.ListInstanceConfigsRequest;
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand All @@ -43,6 +51,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.threeten.bp.Duration;

/**
* Internal connection API for Google Cloud Spanner. This class may introduce breaking changes
Expand Down Expand Up @@ -731,6 +740,65 @@ static List<String> parseProperties(String uri) {
return res;
}

/**
* Executes a quick check to see if this connection can actually connect to a local emulator host
* or other (mock) test server, if the options point to localhost instead of Cloud Spanner.
*/
private void checkLocalConnection() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we extract a class for this? I feel this class is very big already.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I've moved it to a separate class.

final String emulatorHost = System.getenv("SPANNER_EMULATOR_HOST");
String host = getHost() == null ? emulatorHost : getHost();
if (host.startsWith("https://")) {
host = host.substring(8);
}
if (host.startsWith("http://")) {
host = host.substring(7);
}
// Only do the check if the host has been set to localhost.
if (host != null && host.startsWith("localhost") && isUsePlainText()) {
// Do a quick check to see if anything is actually running on the host.
try {
InstanceAdminStubSettings.Builder testEmulatorSettings =
InstanceAdminStubSettings.newBuilder()
.setCredentialsProvider(NoCredentialsProvider.create())
.setTransportChannelProvider(
InstantiatingGrpcChannelProvider.newBuilder().setEndpoint(host).build());
testEmulatorSettings
.listInstanceConfigsSettings()
.setSimpleTimeoutNoRetries(Duration.ofSeconds(10L));
try (GrpcInstanceAdminStub stub =
GrpcInstanceAdminStub.create(testEmulatorSettings.build())) {
stub.listInstanceConfigsCallable()
.call(
ListInstanceConfigsRequest.newBuilder()
.setParent(String.format("projects/%s", getProjectId()))
.build());
}
} catch (UnavailableException e) {
String msg;
if (getHost() != null) {
msg =
String.format(
"The connection string '%s' contains host '%s', but no running"
+ " emulator or other server could be found at that address.\n"
+ "Please check the connection string and/or that the emulator is running.",
getUri(), host);
} else {
msg =
String.format(
"The environment variable SPANNER_EMULATOR_HOST has been set to '%s', but no running"
+ " emulator or other server could be found at that address.\n"
+ "Please check the environment variable and/or that the emulator is running.",
emulatorHost);
}
throw SpannerExceptionFactory.newSpannerException(ErrorCode.UNAVAILABLE, msg);
} catch (UnimplementedException e) {
// Ignore, this is probably a local mock server.
} catch (IOException e) {
// Ignore, this method is not checking whether valid credentials have been set.
}
}
}

/**
* Create a new {@link Connection} from this {@link ConnectionOptions}. Calling this method
* multiple times for the same {@link ConnectionOptions} will return multiple instances of {@link
Expand All @@ -739,6 +807,7 @@ static List<String> parseProperties(String uri) {
* @return a new {@link Connection} to the database referenced by this {@link ConnectionOptions}
*/
public Connection getConnection() {
checkLocalConnection();
return new ConnectionImpl(this);
}

Expand Down
Expand Up @@ -55,8 +55,18 @@ GoogleCredentials createCredentials(String credentialsUrl) {
return getCredentialsFromUrl(credentialsUrl);
}
} catch (IOException e) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT, "Invalid credentials path specified", e);
String msg = "Invalid credentials path specified: ";
if (credentialsUrl == null) {
msg =
msg
+ "There are no credentials set in the connection string, "
+ "and the default application credentials are not set or are pointing to an invalid or non-existing file.\n"
+ "Please check the GOOGLE_APPLICATION_CREDENTIALS environment variable and/or "
+ "the credentials that have been set using the Google Cloud SDK gcloud auth application-default login command";
} else {
msg = msg + credentialsUrl;
}
throw SpannerExceptionFactory.newSpannerException(ErrorCode.INVALID_ARGUMENT, msg, e);
}
}

Expand Down
Expand Up @@ -24,6 +24,8 @@
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.NoCredentials;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerOptions;
import java.util.Arrays;
import org.junit.Test;
Expand Down Expand Up @@ -476,4 +478,34 @@ public void testMaxSessions() {
assertThat(options.getMaxSessions()).isEqualTo(4000);
assertThat(options.getSessionPoolOptions().getMaxSessions()).isEqualTo(4000);
}

@Test
public void testLocalConnectionError() {
String uri =
"cloudspanner://localhost:1/projects/test-project/instances/test-instance/databases/test-database?usePlainText=true";
ConnectionOptions options = ConnectionOptions.newBuilder().setUri(uri).build();
try (Connection connection = options.getConnection()) {
fail("Missing expected exception");
} catch (SpannerException e) {
assertEquals(ErrorCode.UNAVAILABLE, e.getErrorCode());
assertThat(e.getMessage())
.contains(
String.format(
"The connection string '%s' contains host 'localhost:1', but no running", uri));
}
}

@Test
public void testInvalidCredentials() {
String uri =
"cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?credentials=/some/non/existing/path";
try {
ConnectionOptions.newBuilder().setUri(uri).build();
fail("Missing expected exception");
} catch (SpannerException e) {
assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
assertThat(e.getMessage())
.contains("Invalid credentials path specified: /some/non/existing/path");
}
}
}