From 7dacfdc7ca1219a0ddf5929d7b46860b46e3c300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 29 Mar 2021 01:41:46 +0200 Subject: [PATCH] docs: improve error messages (#1011) * docs: improve error messages Improve error messages when something is wrong in the connection string. Fixes #java-spanner-jdbc/399 * fix: fix error message if emulator host is set * refactor: move local connection checker to separate class --- .../spanner/connection/ConnectionOptions.java | 3 + .../connection/CredentialsService.java | 14 ++- .../connection/LocalConnectionChecker.java | 98 +++++++++++++++++++ .../connection/ConnectionOptionsTest.java | 32 ++++++ 4 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/LocalConnectionChecker.java diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java index 46783e0f36..48d60d86c7 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java @@ -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; @@ -739,6 +741,7 @@ static List 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); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/CredentialsService.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/CredentialsService.java index 5cda271a99..f72499810a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/CredentialsService.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/CredentialsService.java @@ -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); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/LocalConnectionChecker.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/LocalConnectionChecker.java new file mode 100644 index 0000000000..edee8a1944 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/LocalConnectionChecker.java @@ -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. + } + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java index b8a0f6aa91..3ab5bbddb4 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java @@ -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; @@ -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"); + } + } }