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 all 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 @@ -144,6 +144,8 @@ public String[] getValidValues() {
}
}

private static final LocalConnectionChecker LOCAL_CONNECTION_CHECKER =
new LocalConnectionChecker();
private static final boolean DEFAULT_USE_PLAIN_TEXT = false;
static final boolean DEFAULT_AUTOCOMMIT = true;
static final boolean DEFAULT_READONLY = false;
Expand Down Expand Up @@ -739,6 +741,7 @@ static List<String> parseProperties(String uri) {
* @return a new {@link Connection} to the database referenced by this {@link ConnectionOptions}
*/
public Connection getConnection() {
LOCAL_CONNECTION_CHECKER.checkLocalConnection(this);
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
@@ -0,0 +1,98 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.spanner.connection;

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.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.admin.instance.v1.stub.GrpcInstanceAdminStub;
import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings;
import com.google.spanner.admin.instance.v1.ListInstanceConfigsRequest;
import java.io.IOException;
import org.threeten.bp.Duration;

/**
* Util class for quickly checking whether a local emulator or test server can be found. A common
* configuration error is to add 'localhost' to the connection string or to forget to unset the
* SPANNER_EMULATOR_HOST environment variable. This can cause cryptic error messages. This util
* checks for common configurations and errors and returns a more understandable error message for
* known misconfigurations.
*/
class LocalConnectionChecker {

/**
* 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.
*/
void checkLocalConnection(ConnectionOptions options) {
final String emulatorHost = System.getenv("SPANNER_EMULATOR_HOST");
String host = options.getHost() == null ? emulatorHost : options.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") && options.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", options.getProjectId()))
.build());
}
} catch (UnavailableException e) {
String msg;
if (options.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.",
options.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.
}
}
}
}
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");
}
}
}