diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000000..f736df80665 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,21 @@ +# Reference - https://github.com/microsoft/vscode-dev-containers/tree/main/containers/java-8/.devcontainer +# [Choice] Debian OS version (use bullseye on local arm64/Apple Silicon): buster, bullseye +# [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 11, 17, 11-bullseye, 17-bullseye, 11-buster, 17-buster +ARG VARIANT="bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/java:11-${VARIANT} + +# [Option] Install Maven +ARG INSTALL_MAVEN="true" +ARG MAVEN_VERSION="" +# [Option] Install Gradle +ARG INSTALL_GRADLE="false" +ARG GRADLE_VERSION="" +RUN if [ "${INSTALL_MAVEN}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\""; fi \ + && if [ "${INSTALL_GRADLE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \"${GRADLE_VERSION}\""; fi + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="lts/*" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# install gloud sdk +RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - && apt-get update -y && apt-get install google-cloud-cli -y diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..4e69cd5b453 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,43 @@ +{ + "name": "Java 11", + "build": { + "dockerfile": "Dockerfile", + "args": { + // Use the VARIANT arg to pick a Debian OS version: buster, bullseye + // Use bullseye when running on local arm64/Apple Silicon. + "VARIANT": "bullseye", + // Options + "INSTALL_MAVEN": "true", + "INSTALL_GRADLE": "false", + "NODE_VERSION": "lts/*" + } + }, + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "java.import.gradle.java.home": "/usr/local/sdkman/candidates/java/current", + "java.configuration.runtimes": [{ + "default": true, + "name": "JavaSE-11", + "path": "/usr/local/sdkman/candidates/java/current" + }] + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "vscjava.vscode-java-pack", + "ms-azuretools.vscode-docker" + ] + } + }, + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "bash .devcontainer/postCreate.sh", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh new file mode 100644 index 00000000000..8dc53f690c3 --- /dev/null +++ b/.devcontainer/postCreate.sh @@ -0,0 +1,6 @@ +echo "Post Create Starting" + +mvn clean install -B -V -ntp \ + -DskipTests=true \ + -Dmaven.javadoc.skip=true \ + -Dclirr.skip=true diff --git a/CHANGELOG.md b/CHANGELOG.md index ac8a686719a..91ffdf41128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [6.47.0](https://github.com/googleapis/java-spanner/compare/v6.46.0...v6.47.0) (2023-09-12) + + +### Features + +* Add devcontainers for enabling github codespaces usage. ([#2605](https://github.com/googleapis/java-spanner/issues/2605)) ([a7d60f1](https://github.com/googleapis/java-spanner/commit/a7d60f13781f87054a1631ca511492c5c8334751)) +* Disable dynamic code loading properties by default ([#2606](https://github.com/googleapis/java-spanner/issues/2606)) ([d855ebb](https://github.com/googleapis/java-spanner/commit/d855ebbd2dec11cdd6cdbe326de81115632598cd)) + + +### Bug Fixes + +* Add reflection configurations for com.google.rpc classes ([#2617](https://github.com/googleapis/java-spanner/issues/2617)) ([c42460a](https://github.com/googleapis/java-spanner/commit/c42460ae7b6bb5874cc18c7aecff34186dcbff2a)) +* Avoid unbalanced session pool creation ([#2442](https://github.com/googleapis/java-spanner/issues/2442)) ([db751ce](https://github.com/googleapis/java-spanner/commit/db751ceebc8b6981d00cd07ce4742196cc1dd50d)) + + +### Dependencies + +* Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.15.0 ([#2615](https://github.com/googleapis/java-spanner/issues/2615)) ([ac762fb](https://github.com/googleapis/java-spanner/commit/ac762fbf079db79eab5f2ebee971b850ac89eb11)) + ## [6.46.0](https://github.com/googleapis/java-spanner/compare/v6.45.3...v6.46.0) (2023-09-06) diff --git a/README.md b/README.md index 5f5972cf193..1e9cfcc7777 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-spanner' If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-spanner:6.45.3' +implementation 'com.google.cloud:google-cloud-spanner:6.46.0' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.45.3" +libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.46.0" ``` @@ -431,7 +431,7 @@ Java is a registered trademark of Oracle and/or its affiliates. [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java11.html [stability-image]: https://img.shields.io/badge/stability-stable-green [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-spanner.svg -[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.45.3 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.46.0 [authentication]: https://github.com/googleapis/google-cloud-java#authentication [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index f1389078ea2..f5eb2533fa3 100644 --- a/google-cloud-spanner-bom/pom.xml +++ b/google-cloud-spanner-bom/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner-bom - 6.46.0 + 6.47.0 pom com.google.cloud @@ -53,48 +53,48 @@ com.google.cloud google-cloud-spanner - 6.46.0 + 6.47.0 com.google.cloud google-cloud-spanner-executor - 6.46.0 + 6.47.0 com.google.cloud google-cloud-spanner test-jar - 6.46.0 + 6.47.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.46.0 + 6.47.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.46.0 + 6.47.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.46.0 + 6.47.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.46.0 + 6.47.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.46.0 + 6.47.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.46.0 + 6.47.0 diff --git a/google-cloud-spanner-executor/pom.xml b/google-cloud-spanner-executor/pom.xml index 7d166579c08..77cfa3836f2 100644 --- a/google-cloud-spanner-executor/pom.xml +++ b/google-cloud-spanner-executor/pom.xml @@ -5,14 +5,14 @@ 4.0.0 com.google.cloud google-cloud-spanner-executor - 6.46.0 + 6.47.0 jar Google Cloud Spanner Executor com.google.cloud google-cloud-spanner-parent - 6.46.0 + 6.47.0 diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 6b80663eead..200d1b9407d 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner - 6.46.0 + 6.47.0 jar Google Cloud Spanner https://github.com/googleapis/java-spanner @@ -11,7 +11,7 @@ com.google.cloud google-cloud-spanner-parent - 6.46.0 + 6.47.0 google-cloud-spanner diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java index f5fa0ebdc48..c97fbf9b643 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java @@ -54,6 +54,7 @@ import com.google.cloud.spanner.SessionPoolOptions.InactiveTransactionRemovalOptions; import com.google.cloud.spanner.SpannerException.ResourceNotFoundException; import com.google.cloud.spanner.SpannerImpl.ClosedException; +import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.MoreObjects; @@ -1366,12 +1367,19 @@ PooledSession get(final boolean eligibleForLongRunning) { } } - final class PooledSession implements Session { + class PooledSession implements Session { @VisibleForTesting SessionImpl delegate; private volatile Instant lastUseTime; private volatile SpannerException lastException; private volatile boolean allowReplacing = true; + /** + * This ensures that the session is added at a random position in the pool the first time it is + * actually added to the pool. + */ + @GuardedBy("lock") + private Position releaseToPosition = initialReleasePosition; + /** * Property to mark if the session is eligible to be long-running. This can only be true if the * session is executing certain types of transactions (for ex - Partitioned DML) which can be @@ -1403,6 +1411,13 @@ private PooledSession(SessionImpl delegate) { this.lastUseTime = clock.instant(); } + int getChannel() { + Long channelHint = (Long) delegate.getOptions().get(SpannerRpc.Option.CHANNEL_HINT); + return channelHint == null + ? 0 + : (int) (channelHint % sessionClient.getSpanner().getOptions().getNumChannels()); + } + @Override public String toString() { return getName(); @@ -1536,7 +1551,7 @@ public void close() { if (state != SessionState.CLOSING) { state = SessionState.AVAILABLE; } - releaseSession(this, Position.FIRST); + releaseSession(this, false); } } @@ -1576,7 +1591,7 @@ private void determineDialectAsync(final SettableFuture dialect) { // in the database dialect, and there's nothing sensible that we can do with it here. dialect.setException(t); } finally { - releaseSession(this, Position.FIRST); + releaseSession(this, false); } }); } @@ -1830,7 +1845,7 @@ private void keepAliveSessions(Instant currTime) { logger.log(Level.FINE, "Keeping alive session " + sessionToKeepAlive.getName()); numSessionsToKeepAlive--; sessionToKeepAlive.keepAlive(); - releaseSession(sessionToKeepAlive, Position.FIRST); + releaseSession(sessionToKeepAlive, false); } catch (SpannerException e) { handleException(e, sessionToKeepAlive); } @@ -1929,7 +1944,7 @@ private void removeLongRunningSessions( } } - private enum Position { + enum Position { FIRST, RANDOM } @@ -1962,6 +1977,15 @@ private enum Position { final PoolMaintainer poolMaintainer; private final Clock clock; + /** + * initialReleasePosition determines where in the pool sessions are added when they are released + * into the pool the first time. This is always RANDOM in production, but some tests use FIRST to + * be able to verify the order of sessions in the pool. Using RANDOM ensures that we do not get an + * unbalanced session pool where all sessions belonging to one gRPC channel are added to the same + * region in the pool. + */ + private final Position initialReleasePosition; + private final Object lock = new Object(); private final Random random = new Random(); @@ -2045,6 +2069,7 @@ static SessionPool createPool( ((GrpcTransportOptions) spannerOptions.getTransportOptions()).getExecutorFactory(), sessionClient, poolMaintainerClock == null ? new Clock() : poolMaintainerClock, + Position.RANDOM, Metrics.getMetricRegistry(), labelValues); } @@ -2053,20 +2078,22 @@ static SessionPool createPool( SessionPoolOptions poolOptions, ExecutorFactory executorFactory, SessionClient sessionClient) { - return createPool(poolOptions, executorFactory, sessionClient, new Clock()); + return createPool(poolOptions, executorFactory, sessionClient, new Clock(), Position.RANDOM); } static SessionPool createPool( SessionPoolOptions poolOptions, ExecutorFactory executorFactory, SessionClient sessionClient, - Clock clock) { + Clock clock, + Position initialReleasePosition) { return createPool( poolOptions, null, executorFactory, sessionClient, clock, + initialReleasePosition, Metrics.getMetricRegistry(), SPANNER_DEFAULT_LABEL_VALUES); } @@ -2077,6 +2104,7 @@ static SessionPool createPool( ExecutorFactory executorFactory, SessionClient sessionClient, Clock clock, + Position initialReleasePosition, MetricRegistry metricRegistry, List labelValues) { SessionPool pool = @@ -2087,6 +2115,7 @@ static SessionPool createPool( executorFactory.get(), sessionClient, clock, + initialReleasePosition, metricRegistry, labelValues); pool.initPool(); @@ -2100,6 +2129,7 @@ private SessionPool( ScheduledExecutorService executor, SessionClient sessionClient, Clock clock, + Position initialReleasePosition, MetricRegistry metricRegistry, List labelValues) { this.options = options; @@ -2108,6 +2138,7 @@ private SessionPool( this.executor = executor; this.sessionClient = sessionClient; this.clock = clock; + this.initialReleasePosition = initialReleasePosition; this.poolMaintainer = new PoolMaintainer(); this.initMetricsCollection(metricRegistry, labelValues); this.waitOnMinSessionsLatch = @@ -2233,7 +2264,7 @@ private void handleException(SpannerException e, PooledSession session) { if (isSessionNotFound(e)) { invalidateSession(session); } else { - releaseSession(session, Position.FIRST); + releaseSession(session, false); } } @@ -2396,26 +2427,38 @@ private void maybeCreateSession() { } } } + /** Releases a session back to the pool. This might cause one of the waiters to be unblocked. */ - private void releaseSession(PooledSession session, Position position) { + private void releaseSession(PooledSession session, boolean isNewSession) { Preconditions.checkNotNull(session); synchronized (lock) { if (closureFuture != null) { return; } if (waiters.size() == 0) { - // No pending waiters - switch (position) { - case RANDOM: - if (!sessions.isEmpty()) { - int pos = random.nextInt(sessions.size() + 1); - sessions.add(pos, session); - break; - } - // fallthrough - case FIRST: - default: - sessions.addFirst(session); + // There are no pending waiters. + // Add to a random position if the head of the session pool already contains many sessions + // with the same channel as this one. + if (session.releaseToPosition == Position.FIRST && isUnbalanced(session)) { + session.releaseToPosition = Position.RANDOM; + } else if (session.releaseToPosition == Position.RANDOM + && !isNewSession + && checkedOutSessions.size() <= 2) { + // Do not randomize if there are few other sessions checked out and this session has been + // used. This ensures that this session will be re-used for the next transaction, which is + // more efficient. + session.releaseToPosition = Position.FIRST; + } + if (session.releaseToPosition == Position.RANDOM && !sessions.isEmpty()) { + // A session should only be added at a random position the first time it is added to + // the pool or if the pool was deemed unbalanced. All following releases into the pool + // should normally happen at the front of the pool (unless the pool is again deemed to be + // unbalanced). + session.releaseToPosition = Position.FIRST; + int pos = random.nextInt(sessions.size() + 1); + sessions.add(pos, session); + } else { + sessions.addFirst(session); } } else { waiters.poll().put(session); @@ -2423,6 +2466,89 @@ private void releaseSession(PooledSession session, Position position) { } } + private boolean isUnbalanced(PooledSession session) { + int channel = session.getChannel(); + int numChannels = sessionClient.getSpanner().getOptions().getNumChannels(); + return isUnbalanced(channel, this.sessions, this.checkedOutSessions, numChannels); + } + + /** + * Returns true if the given list of sessions is considered unbalanced when compared to the + * sessionChannel that is about to be added to the pool. + * + *

The method returns true if all the following is true: + * + *

    + *
  1. The list of sessions is not empty. + *
  2. The number of checked out sessions is > 2. + *
  3. The number of channels being used by the pool is > 1. + *
  4. And at least one of the following is true: + *
      + *
    1. The first numChannels sessions in the list of sessions contains more than 2 + * sessions that use the same channel as the one being added. + *
    2. The list of currently checked out sessions contains more than 2 times the the + * number of sessions with the same channel as the one being added than it should in + * order for it to be perfectly balanced. Perfectly balanced in this case means that + * the list should preferably contain size/numChannels sessions of each channel. + *
    + *
+ * + * @param channelOfSessionBeingAdded the channel number being used by the session that is about to + * be released into the pool + * @param sessions the list of all sessions in the pool + * @param checkedOutSessions the currently checked out sessions of the pool + * @param numChannels the number of channels in use + * @return true if the pool is considered unbalanced, and false otherwise + */ + @VisibleForTesting + static boolean isUnbalanced( + int channelOfSessionBeingAdded, + List sessions, + Set checkedOutSessions, + int numChannels) { + // Do not re-balance the pool if the number of checked out sessions is low, as it is + // better to re-use sessions as much as possible in a low-QPS scenario. + if (sessions.isEmpty() || checkedOutSessions.size() <= 2) { + return false; + } + if (numChannels == 1) { + return false; + } + + // Ideally, the first numChannels sessions in the pool should contain exactly one session for + // each channel. + // Check if the first numChannels sessions at the head of the pool already contain more than 2 + // sessions that use the same channel as this one. If so, we re-balance. + // We also re-balance the pool in the specific case that the pool uses 2 channels and the first + // two sessions use those two channels. + int maxSessionsAtHeadOfPool = Math.min(numChannels, 3); + int count = 0; + for (int i = 0; i < Math.min(numChannels, sessions.size()); i++) { + PooledSession otherSession = sessions.get(i); + if (channelOfSessionBeingAdded == otherSession.getChannel()) { + count++; + if (count >= maxSessionsAtHeadOfPool) { + return true; + } + } + } + // Ideally, the use of a channel in the checked out sessions is exactly + // numCheckedOut / numChannels + // We check whether we are more than a factor two away from that perfect distribution. + // If we are, then we re-balance. + count = 0; + int checkedOutThreshold = Math.max(2, 2 * checkedOutSessions.size() / numChannels); + for (PooledSessionFuture otherSession : checkedOutSessions) { + if (otherSession.isDone() && channelOfSessionBeingAdded == otherSession.get().getChannel()) { + count++; + if (count > checkedOutThreshold) { + return true; + } + } + } + return false; + } + private void handleCreateSessionsFailure(SpannerException e, int count) { synchronized (lock) { for (int i = 0; i < count; i++) { @@ -2622,7 +2748,7 @@ public void onSessionReady(SessionImpl session) { // Release the session to a random position in the pool to prevent the case that a batch // of sessions that are affiliated with the same channel are all placed sequentially in // the pool. - releaseSession(pooledSession, Position.RANDOM); + releaseSession(pooledSession, true); } } } 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 a5753eca8ca..47397100acd 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 @@ -36,6 +36,7 @@ import com.google.cloud.spanner.SpannerOptions; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.collect.Sets; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import java.io.IOException; @@ -205,8 +206,14 @@ public String[] getValidValues() { public static final String CREDENTIALS_PROPERTY_NAME = "credentials"; /** Name of the 'encodedCredentials' connection property. */ public static final String ENCODED_CREDENTIALS_PROPERTY_NAME = "encodedCredentials"; + + public static final String ENABLE_ENCODED_CREDENTIALS_SYSTEM_PROPERTY = + "ENABLE_ENCODED_CREDENTIALS"; /** Name of the 'credentialsProvider' connection property. */ public static final String CREDENTIALS_PROVIDER_PROPERTY_NAME = "credentialsProvider"; + + public static final String ENABLE_CREDENTIALS_PROVIDER_SYSTEM_PROPERTY = + "ENABLE_CREDENTIALS_PROVIDER"; /** * OAuth token to use for authentication. Cannot be used in combination with a credentials file. */ @@ -219,6 +226,8 @@ public String[] getValidValues() { public static final String NUM_CHANNELS_PROPERTY_NAME = "numChannels"; /** Name of the 'channelProvider' connection property. */ public static final String CHANNEL_PROVIDER_PROPERTY_NAME = "channelProvider"; + + public static final String ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY = "ENABLE_CHANNEL_PROVIDER"; /** Custom user agent string is only for other Google libraries. */ private static final String USER_AGENT_PROPERTY_NAME = "userAgent"; /** Query optimizer version to use for a connection. */ @@ -248,6 +257,19 @@ public String[] getValidValues() { public static final String MAX_PARTITIONED_PARALLELISM_PROPERTY_NAME = "maxPartitionedParallelism"; + private static final String GUARDED_CONNECTION_PROPERTY_ERROR_MESSAGE = + "%s can only be used if the system property %s has been set to true. " + + "Start the application with the JVM command line option -D%s=true"; + + private static String generateGuardedConnectionPropertyError( + String systemPropertyName, String connectionPropertyName) { + return String.format( + GUARDED_CONNECTION_PROPERTY_ERROR_MESSAGE, + connectionPropertyName, + systemPropertyName, + systemPropertyName); + } + /** All valid connection properties. */ public static final Set VALID_PROPERTIES = Collections.unmodifiableSet( @@ -846,40 +868,64 @@ static boolean parseRetryAbortsInternally(String uri) { @VisibleForTesting static @Nullable String parseEncodedCredentials(String uri) { - return parseUriProperty(uri, ENCODED_CREDENTIALS_PROPERTY_NAME); + String encodedCredentials = parseUriProperty(uri, ENCODED_CREDENTIALS_PROPERTY_NAME); + checkGuardedProperty( + encodedCredentials, + ENABLE_ENCODED_CREDENTIALS_SYSTEM_PROPERTY, + ENCODED_CREDENTIALS_PROPERTY_NAME); + return encodedCredentials; } @VisibleForTesting static @Nullable CredentialsProvider parseCredentialsProvider(String uri) { - String name = parseUriProperty(uri, CREDENTIALS_PROVIDER_PROPERTY_NAME); - if (name != null) { + String credentialsProviderName = parseUriProperty(uri, CREDENTIALS_PROVIDER_PROPERTY_NAME); + checkGuardedProperty( + credentialsProviderName, + ENABLE_CREDENTIALS_PROVIDER_SYSTEM_PROPERTY, + CREDENTIALS_PROVIDER_PROPERTY_NAME); + if (!Strings.isNullOrEmpty(credentialsProviderName)) { try { Class clazz = - (Class) Class.forName(name); + (Class) Class.forName(credentialsProviderName); Constructor constructor = clazz.getDeclaredConstructor(); return constructor.newInstance(); } catch (ClassNotFoundException classNotFoundException) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.INVALID_ARGUMENT, - "Unknown or invalid CredentialsProvider class name: " + name, + "Unknown or invalid CredentialsProvider class name: " + credentialsProviderName, classNotFoundException); } catch (NoSuchMethodException noSuchMethodException) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.INVALID_ARGUMENT, - "Credentials provider " + name + " does not have a public no-arg constructor.", + "Credentials provider " + + credentialsProviderName + + " does not have a public no-arg constructor.", noSuchMethodException); } catch (InvocationTargetException | InstantiationException | IllegalAccessException exception) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.INVALID_ARGUMENT, - "Failed to create an instance of " + name + ": " + exception.getMessage(), + "Failed to create an instance of " + + credentialsProviderName + + ": " + + exception.getMessage(), exception); } } return null; } + private static void checkGuardedProperty( + String value, String systemPropertyName, String connectionPropertyName) { + if (!Strings.isNullOrEmpty(value) + && !Boolean.parseBoolean(System.getProperty(systemPropertyName))) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.FAILED_PRECONDITION, + generateGuardedConnectionPropertyError(systemPropertyName, connectionPropertyName)); + } + } + @VisibleForTesting static @Nullable String parseOAuthToken(String uri) { String value = parseUriProperty(uri, OAUTH_TOKEN_PROPERTY_NAME); @@ -907,6 +953,8 @@ static String parseNumChannels(String uri) { @VisibleForTesting static String parseChannelProvider(String uri) { String value = parseUriProperty(uri, CHANNEL_PROVIDER_PROPERTY_NAME); + checkGuardedProperty( + value, ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY, CHANNEL_PROVIDER_PROPERTY_NAME); return value != null ? value : DEFAULT_CHANNEL_PROVIDER; } diff --git a/google-cloud-spanner/src/main/resources/META-INF/native-image/com.google.cloud.spanner/reflect-config.json b/google-cloud-spanner/src/main/resources/META-INF/native-image/com.google.cloud.spanner/reflect-config.json new file mode 100644 index 00000000000..ddb6ab0a676 --- /dev/null +++ b/google-cloud-spanner/src/main/resources/META-INF/native-image/com.google.cloud.spanner/reflect-config.json @@ -0,0 +1,172 @@ +[ { + "name": "com.google.rpc.LocalizedMessage", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true +}, + { + "name": "com.google.rpc.LocalizedMessage$Builder", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.BadRequest", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.BadRequest$Builder", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.BadRequest$FieldViolation", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.Help", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.Help$Builder", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.Help$Link", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.QuotaFailure", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.QuotaFailure$Builder", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.QuotaFailure$Violation", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.RequestInfo", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.RequestInfo$Builder", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.ResourceInfo", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.ResourceInfo$Builder", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.RetryInfo", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.RetryInfo$Builder", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.ErrorInfo", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.rpc.ErrorInfo$Builder", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] \ No newline at end of file diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java index 3a595358fea..7f8cf5cc1b0 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java @@ -26,16 +26,21 @@ import com.google.api.core.ApiFutures; import com.google.cloud.grpc.GrpcTransportOptions.ExecutorFactory; import com.google.cloud.spanner.SessionPool.Clock; +import com.google.cloud.spanner.spi.v1.SpannerRpc.Option; import com.google.protobuf.Empty; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import org.threeten.bp.Instant; abstract class BaseSessionPoolTest { ScheduledExecutorService mockExecutor; int sessionIndex; + AtomicLong channelHint = new AtomicLong(0L); final class TestExecutorFactory implements ExecutorFactory { @@ -64,6 +69,9 @@ public void release(ScheduledExecutorService executor) { @SuppressWarnings("unchecked") SessionImpl mockSession() { final SessionImpl session = mock(SessionImpl.class); + Map options = new HashMap<>(); + options.put(Option.CHANNEL_HINT, channelHint.getAndIncrement()); + when(session.getOptions()).thenReturn(options); when(session.getName()) .thenReturn( "projects/dummy/instances/dummy/database/dummy/sessions/session" + sessionIndex); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java index ab6a51c9260..2f7b14fdadf 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java @@ -26,6 +26,7 @@ import com.google.cloud.spanner.SessionClient.SessionConsumer; import com.google.cloud.spanner.SessionPool.PooledSession; import com.google.cloud.spanner.SessionPool.PooledSessionFuture; +import com.google.cloud.spanner.SessionPool.Position; import com.google.cloud.spanner.SessionPool.SessionConsumerImpl; import java.util.ArrayList; import java.util.HashMap; @@ -58,6 +59,7 @@ public void setUp() { initMocks(this); when(client.getOptions()).thenReturn(spannerOptions); when(client.getSessionClient(db)).thenReturn(sessionClient); + when(sessionClient.getSpanner()).thenReturn(client); when(spannerOptions.getNumChannels()).thenReturn(4); when(spannerOptions.getDatabaseRole()).thenReturn("role"); setupMockSessionCreation(); @@ -111,9 +113,11 @@ private SessionImpl setupMockSession(final SessionImpl session) { } private SessionPool createPool() throws Exception { + // Allow sessions to be added to the head of the pool in all cases in this test, as it is + // otherwise impossible to know which session exactly is getting pinged at what point in time. SessionPool pool = SessionPool.createPool( - options, new TestExecutorFactory(), client.getSessionClient(db), clock); + options, new TestExecutorFactory(), client.getSessionClient(db), clock, Position.FIRST); pool.idleSessionRemovedListener = input -> { idledSessions.add(input); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java index d1aab02d32f..c9ba9b360af 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java @@ -25,6 +25,7 @@ import com.google.api.core.ApiFutures; import com.google.cloud.spanner.SessionClient.SessionConsumer; import com.google.cloud.spanner.SessionPool.PooledSessionFuture; +import com.google.cloud.spanner.SessionPool.Position; import com.google.cloud.spanner.SessionPool.SessionConsumerImpl; import com.google.cloud.spanner.SessionPoolOptions.ActionOnInactiveTransaction; import com.google.cloud.spanner.SessionPoolOptions.InactiveTransactionRemovalOptions; @@ -39,6 +40,7 @@ import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -65,10 +67,10 @@ public class SessionPoolStressTest extends BaseSessionPoolTest { SessionPool pool; SessionPoolOptions options; ExecutorService createExecutor = Executors.newSingleThreadExecutor(); - Object lock = new Object(); + final Object lock = new Object(); Random random = new Random(); FakeClock clock = new FakeClock(); - Map sessions = new HashMap<>(); + final Map sessions = new ConcurrentHashMap<>(); // Exception keeps track of where the session was closed at. Map closedSessions = new HashMap<>(); Set expiredSessions = new HashSet<>(); @@ -92,6 +94,7 @@ private void setupSpanner(DatabaseId db) { when(spannerOptions.getNumChannels()).thenReturn(4); when(spannerOptions.getDatabaseRole()).thenReturn("role"); SessionClient sessionClient = mock(SessionClient.class); + when(sessionClient.getSpanner()).thenReturn(mockSpanner); when(mockSpanner.getSessionClient(db)).thenReturn(sessionClient); when(mockSpanner.getOptions()).thenReturn(spannerOptions); doAnswer( @@ -226,22 +229,26 @@ public void stressTest() throws Exception { } pool = SessionPool.createPool( - builder.build(), new TestExecutorFactory(), mockSpanner.getSessionClient(db), clock); + builder.build(), + new TestExecutorFactory(), + mockSpanner.getSessionClient(db), + clock, + Position.RANDOM); pool.idleSessionRemovedListener = pooled -> { String name = pooled.getName(); - synchronized (lock) { - sessions.remove(name); - return null; - } + // We do not take the test lock here, as we already hold the session pool lock. Taking the + // test lock as well here can cause a deadlock. + sessions.remove(name); + return null; }; pool.longRunningSessionRemovedListener = pooled -> { String name = pooled.getName(); - synchronized (lock) { - sessions.remove(name); - return null; - } + // We do not take the test lock here, as we already hold the session pool lock. Taking the + // test lock as well here can cause a deadlock. + sessions.remove(name); + return null; }; for (int i = 0; i < concurrentThreads; i++) { new Thread( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java index b20d5dd652b..8949ba6afa8 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java @@ -52,6 +52,7 @@ import com.google.cloud.spanner.SessionPool.Clock; import com.google.cloud.spanner.SessionPool.PooledSession; import com.google.cloud.spanner.SessionPool.PooledSessionFuture; +import com.google.cloud.spanner.SessionPool.Position; import com.google.cloud.spanner.SessionPool.SessionConsumerImpl; import com.google.cloud.spanner.SpannerImpl.ClosedException; import com.google.cloud.spanner.TransactionRunner.TransactionCallable; @@ -126,7 +127,7 @@ private SessionPool createPool() { private SessionPool createPool(Clock clock) { return SessionPool.createPool( - options, new TestExecutorFactory(), client.getSessionClient(db), clock); + options, new TestExecutorFactory(), client.getSessionClient(db), clock, Position.RANDOM); } private SessionPool createPool( @@ -137,6 +138,7 @@ private SessionPool createPool( new TestExecutorFactory(), client.getSessionClient(db), clock, + Position.RANDOM, metricRegistry, labelValues); } @@ -146,6 +148,7 @@ public void setUp() { initMocks(this); when(client.getOptions()).thenReturn(spannerOptions); when(client.getSessionClient(db)).thenReturn(sessionClient); + when(sessionClient.getSpanner()).thenReturn(client); when(spannerOptions.getNumChannels()).thenReturn(4); when(spannerOptions.getDatabaseRole()).thenReturn("role"); options = @@ -204,13 +207,27 @@ public void sessionCreation() { @Test public void poolLifo() { setupMockSessionCreation(); + options = + options + .toBuilder() + .setMinSessions(2) + .setWaitForMinSessions(Duration.ofSeconds(10L)) + .build(); pool = createPool(); + pool.maybeWaitOnMinSessions(); Session session1 = pool.getSession().get(); Session session2 = pool.getSession().get(); assertThat(session1).isNotEqualTo(session2); session2.close(); session1.close(); + + // Check the session out and back in once more to finalize their positions. + session1 = pool.getSession().get(); + session2 = pool.getSession().get(); + session2.close(); + session1.close(); + Session session3 = pool.getSession().get(); Session session4 = pool.getSession().get(); assertThat(session3).isEqualTo(session1); @@ -1187,6 +1204,7 @@ public void testSessionNotFoundReadWriteTransaction() { SpannerImpl spanner = mock(SpannerImpl.class); SessionClient sessionClient = mock(SessionClient.class); when(spanner.getSessionClient(db)).thenReturn(sessionClient); + when(sessionClient.getSpanner()).thenReturn(spanner); doAnswer( invocation -> { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolUnbalancedTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolUnbalancedTest.java new file mode 100644 index 00000000000..5a9365eaed9 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolUnbalancedTest.java @@ -0,0 +1,241 @@ +/* + * Copyright 2023 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; + +import static com.google.cloud.spanner.SessionPool.isUnbalanced; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.cloud.spanner.SessionPool.PooledSession; +import com.google.cloud.spanner.SessionPool.PooledSessionFuture; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SessionPoolUnbalancedTest { + + static PooledSession mockedSession(int channel) { + PooledSession session = mock(PooledSession.class); + when(session.getChannel()).thenReturn(channel); + return session; + } + + static List mockedSessions(int... channels) { + return Arrays.stream(channels) + .mapToObj(SessionPoolUnbalancedTest::mockedSession) + .collect(Collectors.toList()); + } + + static PooledSessionFuture mockedCheckedOutSession(int channel) { + PooledSession session = mockedSession(channel); + PooledSessionFuture future = mock(PooledSessionFuture.class); + when(future.get()).thenReturn(session); + when(future.isDone()).thenReturn(true); + return future; + } + + static Set mockedCheckedOutSessions(int... channels) { + return Arrays.stream(channels) + .mapToObj(SessionPoolUnbalancedTest::mockedCheckedOutSession) + .collect(Collectors.toSet()); + } + + @Test + public void testIsUnbalancedBasics() { + // An empty session pool is never unbalanced. + assertFalse(isUnbalanced(1, mockedSessions(), mockedCheckedOutSessions(1, 1, 1), 1)); + assertFalse(isUnbalanced(1, mockedSessions(), mockedCheckedOutSessions(1, 1, 1), 2)); + assertFalse(isUnbalanced(1, mockedSessions(), mockedCheckedOutSessions(1, 1, 1), 4)); + assertFalse(isUnbalanced(1, mockedSessions(), mockedCheckedOutSessions(1, 1, 1, 1), 1)); + assertFalse(isUnbalanced(1, mockedSessions(), mockedCheckedOutSessions(1, 1, 1, 1), 2)); + assertFalse(isUnbalanced(1, mockedSessions(), mockedCheckedOutSessions(1, 1, 1, 1), 4)); + assertFalse(isUnbalanced(1, mockedSessions(), mockedCheckedOutSessions(1, 1, 1, 1, 1), 1)); + assertFalse(isUnbalanced(1, mockedSessions(), mockedCheckedOutSessions(1, 1, 1, 1, 1), 2)); + assertFalse(isUnbalanced(1, mockedSessions(), mockedCheckedOutSessions(1, 1, 1, 1, 1), 4)); + + // A session pool that has 2 or fewer sessions checked out is never unbalanced. + // This prevents low-QPS scenarios from re-balancing the pool. + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1), mockedCheckedOutSessions(), 1)); + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1), mockedCheckedOutSessions(), 2)); + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1), mockedCheckedOutSessions(), 4)); + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1, 1), mockedCheckedOutSessions(1), 1)); + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1, 1), mockedCheckedOutSessions(1), 2)); + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1, 1), mockedCheckedOutSessions(1), 4)); + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1, 1, 1), mockedCheckedOutSessions(1, 1), 1)); + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1, 1, 1), mockedCheckedOutSessions(1, 1), 2)); + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1, 1, 1), mockedCheckedOutSessions(1, 1), 4)); + + // A session pool that uses only 1 channel is never unbalanced. + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1), mockedCheckedOutSessions(), 1)); + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1, 1), mockedCheckedOutSessions(), 1)); + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1, 1, 1), mockedCheckedOutSessions(), 1)); + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1, 1, 1, 1), mockedCheckedOutSessions(), 1)); + assertFalse(isUnbalanced(1, mockedSessions(1, 1, 1), mockedCheckedOutSessions(1, 1, 1), 1)); + assertFalse( + isUnbalanced(1, mockedSessions(1, 1, 1, 1), mockedCheckedOutSessions(1, 1, 1, 1), 1)); + assertFalse( + isUnbalanced(1, mockedSessions(1, 1, 1, 1, 1), mockedCheckedOutSessions(1, 1, 1, 1, 1), 1)); + assertFalse( + isUnbalanced( + 1, mockedSessions(1, 1, 1, 1, 1, 1), mockedCheckedOutSessions(1, 1, 1, 1, 1, 1), 1)); + } + + @Test + public void testIsUnbalanced_returnsFalseForBalancedPool() { + assertFalse( + isUnbalanced(1, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(1, 2, 3, 4), 4)); + assertFalse( + isUnbalanced(2, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(1, 2, 3, 4), 4)); + assertFalse( + isUnbalanced(3, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(1, 2, 3, 4), 4)); + assertFalse( + isUnbalanced(4, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(1, 2, 3, 4), 4)); + + assertFalse( + isUnbalanced(1, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(4, 3, 2, 1), 4)); + assertFalse( + isUnbalanced(2, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(4, 3, 2, 1), 4)); + assertFalse( + isUnbalanced(3, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(4, 3, 2, 1), 4)); + assertFalse( + isUnbalanced(4, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(4, 3, 2, 1), 4)); + + assertFalse( + isUnbalanced( + 1, + mockedSessions(1, 2, 3, 4, 1, 2, 3, 4), + mockedCheckedOutSessions(1, 2, 3, 4, 1, 2, 3, 4), + 4)); + + // We only check the first numChannels sessions that are in the pool, so the fact that the end + // of the pool is unbalanced is not a reason to re-balance. + assertFalse( + isUnbalanced( + 1, mockedSessions(1, 2, 3, 4, 1, 1, 1, 1), mockedCheckedOutSessions(1, 2, 3, 4), 4)); + assertFalse( + isUnbalanced(1, mockedSessions(1, 2, 1, 1, 1, 1), mockedCheckedOutSessions(1, 2), 2)); + assertFalse( + isUnbalanced( + 1, + mockedSessions(1, 2, 3, 4, 1, 2, 3, 4, 1, 1, 1, 1), + mockedCheckedOutSessions(1, 2, 3, 4), + 8)); + assertFalse( + isUnbalanced( + 1, + mockedSessions(1, 1, 2, 2, 3, 3, 4, 4, 1, 1, 1, 1), + mockedCheckedOutSessions(1, 2, 3, 4), + 8)); + + // The list of checked out sessions is allowed to contain up to twice the number of sessions + // with a given channel than it should for a perfect distribution (perfect means + // num_sessions_with_a_channel == num_channels). + assertFalse( + isUnbalanced(1, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(1, 1, 2, 3), 4)); + assertFalse( + isUnbalanced( + 1, + mockedSessions(1, 2, 3, 4), + mockedCheckedOutSessions(1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 2, 3, 4, 5, 6), + 8)); + // We're only checking the list of checked out sessions against the channel that is being added + // to the pool. + assertFalse( + isUnbalanced(1, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(2, 2, 2, 2), 4)); + + // We do not consider a pool unbalanced if the list of checked out sessions only contains 2 of + // the same channel, even if that would still be 'more than twice the ideal number'. This + // prevents that a small number of checked out sessions that happen to use the same channel + // causes the pool to be considered unbalanced. + assertFalse( + isUnbalanced( + 1, mockedSessions(1, 2, 3, 4, 5, 6, 7, 8), mockedCheckedOutSessions(1, 1, 2), 8)); + + // A larger number of checked out sessions means that we can also have a 'large' number of the + // same channels in that list, as long as it does not exceed twice the number that it should be + // for an ideal distribution. + assertFalse( + isUnbalanced( + 1, + mockedSessions(1, 2, 3, 4, 5, 6, 7, 8), + mockedCheckedOutSessions(1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 2, 4, 5, 5, 3, 4, 8, 8), + 8)); + } + + @Test + public void testIsUnbalanced_returnsTrueForUnbalancedPool() { + // The pool is considered unbalanced if the first numChannel sessions contain 3 or more of the + // same sessions as the one that is being added. Also; if the pool uses only 2 channels, then it + // is also considered unbalanced if the two first sessions in the pool already use the same + // channel as the one being added. + assertTrue(isUnbalanced(1, mockedSessions(1, 1), mockedCheckedOutSessions(1, 2, 1, 2), 2)); + assertTrue(isUnbalanced(2, mockedSessions(2, 2), mockedCheckedOutSessions(1, 2, 1, 2), 2)); + + assertTrue( + isUnbalanced(1, mockedSessions(1, 1, 1, 4), mockedCheckedOutSessions(1, 2, 3, 4), 4)); + assertTrue( + isUnbalanced(2, mockedSessions(2, 2, 2, 4), mockedCheckedOutSessions(1, 2, 3, 4), 4)); + assertTrue( + isUnbalanced(3, mockedSessions(1, 3, 3, 3), mockedCheckedOutSessions(1, 2, 3, 4), 4)); + assertTrue( + isUnbalanced(4, mockedSessions(1, 4, 4, 4), mockedCheckedOutSessions(1, 2, 3, 4), 4)); + + assertTrue( + isUnbalanced( + 1, mockedSessions(1, 2, 3, 4, 5, 6, 1, 1), mockedCheckedOutSessions(1, 2, 3, 4), 8)); + assertTrue( + isUnbalanced( + 2, mockedSessions(1, 3, 4, 5, 6, 2, 2, 2), mockedCheckedOutSessions(1, 2, 3, 4), 8)); + assertTrue( + isUnbalanced( + 3, mockedSessions(1, 2, 3, 3, 4, 5, 3, 6), mockedCheckedOutSessions(1, 2, 3, 4), 8)); + assertTrue( + isUnbalanced( + 4, mockedSessions(1, 2, 3, 4, 5, 4, 5, 4), mockedCheckedOutSessions(1, 2, 3, 4), 8)); + + // The pool is also considered unbalanced if the list of checked out sessions contain more than + // 2 times as many sessions of the one being returned as it should. + assertTrue( + isUnbalanced(1, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(1, 1, 2, 1), 4)); + assertTrue( + isUnbalanced(2, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(2, 2, 2, 4), 4)); + assertTrue( + isUnbalanced(3, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(1, 3, 3, 3), 4)); + assertTrue( + isUnbalanced(4, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(4, 2, 4, 4), 4)); + assertTrue( + isUnbalanced( + 1, mockedSessions(1, 2, 3, 4), mockedCheckedOutSessions(1, 1, 2, 1, 1, 2, 3, 1), 4)); + + assertTrue( + isUnbalanced( + 1, mockedSessions(1, 2, 3, 4, 5, 6, 7, 8), mockedCheckedOutSessions(1, 1, 1, 3), 8)); + assertTrue( + isUnbalanced( + 1, + mockedSessions(1, 2, 3, 4, 5, 6, 7, 8), + mockedCheckedOutSessions(1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 1, 1), + 8)); + } +} 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 50b376f5ac5..7690ea3934c 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 @@ -36,18 +36,19 @@ import com.google.common.io.BaseEncoding; import com.google.common.io.Files; import java.io.File; -import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; +import java.util.Objects; import org.junit.Test; +import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class ConnectionOptionsTest { private static final String FILE_TEST_PATH = - ConnectionOptionsTest.class.getResource("test-key.json").getFile(); + Objects.requireNonNull(ConnectionOptionsTest.class.getResource("test-key.json")).getFile(); private static final String DEFAULT_HOST = "https://spanner.googleapis.com"; private static final String TEST_PROJECT = "test-project-123"; private static final String TEST_INSTANCE = "test-instance-123"; @@ -727,34 +728,61 @@ public void testInvalidCredentials() { } @Test - public void testNonBase64EncodedCredentials() { - String uri = - "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?encodedCredentials=not-a-base64-string/"; - SpannerException e = - assertThrows( - SpannerException.class, () -> ConnectionOptions.newBuilder().setUri(uri).build()); - assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); - assertThat(e.getMessage()) - .contains("The encoded credentials could not be decoded as a base64 string."); - } - - @Test - public void testInvalidEncodedCredentials() throws UnsupportedEncodingException { - String uri = - String.format( - "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?encodedCredentials=%s", - BaseEncoding.base64Url().encode("not-a-credentials-JSON-string".getBytes("UTF-8"))); - SpannerException e = - assertThrows( - SpannerException.class, () -> ConnectionOptions.newBuilder().setUri(uri).build()); - assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); - assertThat(e.getMessage()) - .contains( - "The encoded credentials do not contain a valid Google Cloud credentials JSON string."); - } - - @Test - public void testValidEncodedCredentials() throws Exception { + public void testNonBase64EncodedCredentials() throws Throwable { + runWithSystemPropertyEnabled( + ConnectionOptions.ENABLE_ENCODED_CREDENTIALS_SYSTEM_PROPERTY, + () -> { + String uri = + "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?encodedCredentials=not-a-base64-string/"; + SpannerException e = + assertThrows( + SpannerException.class, () -> ConnectionOptions.newBuilder().setUri(uri).build()); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertThat(e.getMessage()) + .contains("The encoded credentials could not be decoded as a base64 string."); + }); + } + + @Test + public void testInvalidEncodedCredentials() throws Throwable { + runWithSystemPropertyEnabled( + ConnectionOptions.ENABLE_ENCODED_CREDENTIALS_SYSTEM_PROPERTY, + () -> { + String uri = + String.format( + "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?encodedCredentials=%s", + BaseEncoding.base64Url() + .encode("not-a-credentials-JSON-string".getBytes(StandardCharsets.UTF_8))); + SpannerException e = + assertThrows( + SpannerException.class, () -> ConnectionOptions.newBuilder().setUri(uri).build()); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertThat(e.getMessage()) + .contains( + "The encoded credentials do not contain a valid Google Cloud credentials JSON string."); + }); + } + + @Test + public void testValidEncodedCredentials() throws Throwable { + runWithSystemPropertyEnabled( + ConnectionOptions.ENABLE_ENCODED_CREDENTIALS_SYSTEM_PROPERTY, + () -> { + String encoded = + BaseEncoding.base64Url().encode(Files.asByteSource(new File(FILE_TEST_PATH)).read()); + String uri = + String.format( + "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?encodedCredentials=%s", + encoded); + + ConnectionOptions options = ConnectionOptions.newBuilder().setUri(uri).build(); + assertEquals( + new CredentialsService().createCredentials(FILE_TEST_PATH), options.getCredentials()); + }); + } + + @Test + public void testValidEncodedCredentials_WithoutEnablingProperty() throws Throwable { String encoded = BaseEncoding.base64Url().encode(Files.asByteSource(new File(FILE_TEST_PATH)).read()); String uri = @@ -762,81 +790,146 @@ public void testValidEncodedCredentials() throws Exception { "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?encodedCredentials=%s", encoded); - ConnectionOptions options = ConnectionOptions.newBuilder().setUri(uri).build(); - assertEquals( - new CredentialsService().createCredentials(FILE_TEST_PATH), options.getCredentials()); + SpannerException exception = + assertThrows( + SpannerException.class, () -> ConnectionOptions.newBuilder().setUri(uri).build()); + assertEquals(ErrorCode.FAILED_PRECONDITION, exception.getErrorCode()); } @Test - public void testSetCredentialsAndEncodedCredentials() throws Exception { - String encoded = - BaseEncoding.base64Url().encode(Files.asByteSource(new File(FILE_TEST_PATH)).read()); - String uri = - String.format( - "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?credentials=%s;encodedCredentials=%s", - FILE_TEST_PATH, encoded); + public void testSetCredentialsAndEncodedCredentials() throws Throwable { + runWithSystemPropertyEnabled( + ConnectionOptions.ENABLE_ENCODED_CREDENTIALS_SYSTEM_PROPERTY, + () -> { + String encoded = + BaseEncoding.base64Url().encode(Files.asByteSource(new File(FILE_TEST_PATH)).read()); + String uri = + String.format( + "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?credentials=%s;encodedCredentials=%s", + FILE_TEST_PATH, encoded); - IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, - () -> ConnectionOptions.newBuilder().setUri(uri).build()); - assertTrue( - e.getMessage(), - e.getMessage() - .contains( - "Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth token")); + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> ConnectionOptions.newBuilder().setUri(uri).build()); + assertTrue( + e.getMessage(), + e.getMessage() + .contains( + "Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth token")); + }); } public static class TestCredentialsProvider implements CredentialsProvider { @Override - public Credentials getCredentials() throws IOException { + public Credentials getCredentials() { return NoCredentials.getInstance(); } } + static void runWithSystemPropertyEnabled(String systemPropertyName, ThrowingRunnable runnable) + throws Throwable { + String originalValue = System.getProperty(systemPropertyName); + System.setProperty(systemPropertyName, "true"); + try { + runnable.run(); + } finally { + if (originalValue == null) { + System.clearProperty(systemPropertyName); + } else { + System.setProperty(systemPropertyName, originalValue); + } + } + } + + @Test + public void testValidCredentialsProvider() throws Throwable { + runWithSystemPropertyEnabled( + ConnectionOptions.ENABLE_CREDENTIALS_PROVIDER_SYSTEM_PROPERTY, + () -> { + String uri = + String.format( + "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?credentialsProvider=%s", + TestCredentialsProvider.class.getName()); + + ConnectionOptions options = ConnectionOptions.newBuilder().setUri(uri).build(); + assertEquals(NoCredentials.getInstance(), options.getCredentials()); + }); + } + @Test - public void testValidCredentialsProvider() { + public void testValidCredentialsProvider_WithoutEnablingSystemProperty() { String uri = String.format( "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?credentialsProvider=%s", TestCredentialsProvider.class.getName()); + SpannerException exception = + assertThrows( + SpannerException.class, () -> ConnectionOptions.newBuilder().setUri(uri).build()); + assertEquals(ErrorCode.FAILED_PRECONDITION, exception.getErrorCode()); + assertEquals( + "FAILED_PRECONDITION: credentialsProvider can only be used if the system property ENABLE_CREDENTIALS_PROVIDER has been set to true. " + + "Start the application with the JVM command line option -DENABLE_CREDENTIALS_PROVIDER=true", + exception.getMessage()); + } - ConnectionOptions options = ConnectionOptions.newBuilder().setUri(uri).build(); - assertEquals(NoCredentials.getInstance(), options.getCredentials()); + @Test + public void testSetCredentialsAndCredentialsProvider() throws Throwable { + runWithSystemPropertyEnabled( + ConnectionOptions.ENABLE_CREDENTIALS_PROVIDER_SYSTEM_PROPERTY, + () -> { + String uri = + String.format( + "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?credentials=%s;credentialsProvider=%s", + FILE_TEST_PATH, NoCredentialsProvider.class.getName()); + + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> ConnectionOptions.newBuilder().setUri(uri).build()); + assertTrue( + e.getMessage(), + e.getMessage() + .contains( + "Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth token")); + }); } @Test - public void testSetCredentialsAndCredentialsProvider() { - String uri = - String.format( - "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?credentials=%s;credentialsProvider=%s", - FILE_TEST_PATH, NoCredentialsProvider.class.getName()); + public void testExternalChannelProvider() throws Throwable { + runWithSystemPropertyEnabled( + ConnectionOptions.ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY, + () -> { + String baseUri = + "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"; - IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, - () -> ConnectionOptions.newBuilder().setUri(uri).build()); - assertTrue( - e.getMessage(), - e.getMessage() - .contains( - "Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth token")); + ConnectionOptions options = + ConnectionOptions.newBuilder() + .setUri( + baseUri + + "?channelProvider=com.google.cloud.spanner.connection.TestChannelProvider") + .setCredentials(NoCredentials.getInstance()) + .build(); + + TransportChannelProvider provider = options.getChannelProvider(); + assertTrue(provider instanceof InstantiatingGrpcChannelProvider); + }); } @Test - public void testExternalChannelProvider() { + public void testExternalChannelProvider_WithoutEnablingProperty() throws Throwable { String baseUri = "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"; - - ConnectionOptions options = - ConnectionOptions.newBuilder() - .setUri( - baseUri - + "?channelProvider=com.google.cloud.spanner.connection.TestChannelProvider") - .setCredentials(NoCredentials.getInstance()) - .build(); - - TransportChannelProvider provider = options.getChannelProvider(); - assertTrue(provider instanceof InstantiatingGrpcChannelProvider); + SpannerException exception = + assertThrows( + SpannerException.class, + () -> + ConnectionOptions.newBuilder() + .setUri( + baseUri + + "?channelProvider=com.google.cloud.spanner.connection.TestChannelProvider") + .setCredentials(NoCredentials.getInstance()) + .build()); + assertEquals(ErrorCode.FAILED_PRECONDITION, exception.getErrorCode()); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/CredentialsProviderTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/CredentialsProviderTest.java index da7751f3889..cdab8535029 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/CredentialsProviderTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/CredentialsProviderTest.java @@ -16,13 +16,13 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.connection.ConnectionOptionsTest.runWithSystemPropertyEnabled; import static org.junit.Assert.assertEquals; import com.google.api.gax.core.CredentialsProvider; import com.google.auth.Credentials; import com.google.auth.oauth2.OAuth2Credentials; import io.grpc.ManagedChannelBuilder; -import java.io.IOException; import java.io.ObjectStreamException; import java.util.concurrent.atomic.AtomicInteger; import org.junit.BeforeClass; @@ -64,63 +64,81 @@ public int hashCode() { static final class TestCredentialsProvider implements CredentialsProvider { @Override - public Credentials getCredentials() throws IOException { + public Credentials getCredentials() { return new TestCredentials(COUNTER.incrementAndGet()); } } @Test - public void testCredentialsProvider() { - ConnectionOptions options = - ConnectionOptions.newBuilder() - .setUri( - String.format( - "cloudspanner://localhost:%d/projects/proj/instances/inst/databases/db?credentialsProvider=%s", - getPort(), TestCredentialsProvider.class.getName())) - .setConfigurator( - spannerOptions -> - spannerOptions.setChannelConfigurator(ManagedChannelBuilder::usePlaintext)) - .build(); + public void testCredentialsProvider() throws Throwable { + runWithSystemPropertyEnabled( + ConnectionOptions.ENABLE_CREDENTIALS_PROVIDER_SYSTEM_PROPERTY, + () -> { + ConnectionOptions options = + ConnectionOptions.newBuilder() + .setUri( + String.format( + "cloudspanner://localhost:%d/projects/proj/instances/inst/databases/db?credentialsProvider=%s", + getPort(), TestCredentialsProvider.class.getName())) + .setConfigurator( + spannerOptions -> + spannerOptions.setChannelConfigurator( + ManagedChannelBuilder::usePlaintext)) + .build(); - try (Connection connection = options.getConnection()) { - assertEquals( - TestCredentials.class, - ((ConnectionImpl) connection).getSpanner().getOptions().getCredentials().getClass()); - TestCredentials credentials = - (TestCredentials) - ((ConnectionImpl) connection).getSpanner().getOptions().getCredentials(); - assertEquals(1, credentials.id); - } - // The second connection should get the same credentials from the provider. - try (Connection connection = options.getConnection()) { - assertEquals( - TestCredentials.class, - ((ConnectionImpl) connection).getSpanner().getOptions().getCredentials().getClass()); - TestCredentials credentials = - (TestCredentials) - ((ConnectionImpl) connection).getSpanner().getOptions().getCredentials(); - assertEquals(1, credentials.id); - } + try (Connection connection = options.getConnection()) { + assertEquals( + TestCredentials.class, + ((ConnectionImpl) connection) + .getSpanner() + .getOptions() + .getCredentials() + .getClass()); + TestCredentials credentials = + (TestCredentials) + ((ConnectionImpl) connection).getSpanner().getOptions().getCredentials(); + assertEquals(1, credentials.id); + } + // The second connection should get the same credentials from the provider. + try (Connection connection = options.getConnection()) { + assertEquals( + TestCredentials.class, + ((ConnectionImpl) connection) + .getSpanner() + .getOptions() + .getCredentials() + .getClass()); + TestCredentials credentials = + (TestCredentials) + ((ConnectionImpl) connection).getSpanner().getOptions().getCredentials(); + assertEquals(1, credentials.id); + } - // Creating new ConnectionOptions should refresh the credentials. - options = - ConnectionOptions.newBuilder() - .setUri( - String.format( - "cloudspanner://localhost:%d/projects/proj/instances/inst/databases/db?credentialsProvider=%s", - getPort(), TestCredentialsProvider.class.getName())) - .setConfigurator( - spannerOptions -> - spannerOptions.setChannelConfigurator(ManagedChannelBuilder::usePlaintext)) - .build(); - try (Connection connection = options.getConnection()) { - assertEquals( - TestCredentials.class, - ((ConnectionImpl) connection).getSpanner().getOptions().getCredentials().getClass()); - TestCredentials credentials = - (TestCredentials) - ((ConnectionImpl) connection).getSpanner().getOptions().getCredentials(); - assertEquals(2, credentials.id); - } + // Creating new ConnectionOptions should refresh the credentials. + options = + ConnectionOptions.newBuilder() + .setUri( + String.format( + "cloudspanner://localhost:%d/projects/proj/instances/inst/databases/db?credentialsProvider=%s", + getPort(), TestCredentialsProvider.class.getName())) + .setConfigurator( + spannerOptions -> + spannerOptions.setChannelConfigurator( + ManagedChannelBuilder::usePlaintext)) + .build(); + try (Connection connection = options.getConnection()) { + assertEquals( + TestCredentials.class, + ((ConnectionImpl) connection) + .getSpanner() + .getOptions() + .getCredentials() + .getClass()); + TestCredentials credentials = + (TestCredentials) + ((ConnectionImpl) connection).getSpanner().getOptions().getCredentials(); + assertEquals(2, credentials.id); + } + }); } } diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index efbc3323034..c72c46cf01b 100644 --- a/grpc-google-cloud-spanner-admin-database-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.46.0 + 6.47.0 grpc-google-cloud-spanner-admin-database-v1 GRPC library for grpc-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 6.46.0 + 6.47.0 diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml index 07a676637d4..f79c2e6b1f9 100644 --- a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.46.0 + 6.47.0 grpc-google-cloud-spanner-admin-instance-v1 GRPC library for grpc-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 6.46.0 + 6.47.0 diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index c87f44d3c78..f1b13b9ab8f 100644 --- a/grpc-google-cloud-spanner-v1/pom.xml +++ b/grpc-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.46.0 + 6.47.0 grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.46.0 + 6.47.0 diff --git a/pom.xml b/pom.xml index 0274cdc4543..017960333a8 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 6.46.0 + 6.47.0 Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -54,7 +54,7 @@ UTF-8 github google-cloud-spanner-parent - 3.14.0 + 3.15.0
@@ -62,37 +62,37 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.46.0 + 6.47.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.46.0 + 6.47.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.46.0 + 6.47.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.46.0 + 6.47.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.46.0 + 6.47.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.46.0 + 6.47.0 com.google.cloud google-cloud-spanner - 6.46.0 + 6.47.0 diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml index cf0137536c0..c6eb130f895 100644 --- a/proto-google-cloud-spanner-admin-database-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.46.0 + 6.47.0 proto-google-cloud-spanner-admin-database-v1 PROTO library for proto-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 6.46.0 + 6.47.0 diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml index fb7fc00a3a3..890bed6320c 100644 --- a/proto-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.46.0 + 6.47.0 proto-google-cloud-spanner-admin-instance-v1 PROTO library for proto-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 6.46.0 + 6.47.0 diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 666edb481bc..28c4b027559 100644 --- a/proto-google-cloud-spanner-v1/pom.xml +++ b/proto-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.46.0 + 6.47.0 proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.46.0 + 6.47.0 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index b5e84778899..faddfc846e0 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -31,7 +31,7 @@ com.google.cloud google-cloud-spanner - 6.46.0 + 6.47.0 diff --git a/versions.txt b/versions.txt index 0b08d5572ee..191e5fc7f49 100644 --- a/versions.txt +++ b/versions.txt @@ -1,11 +1,11 @@ # Format: # module:released-version:current-version -proto-google-cloud-spanner-admin-instance-v1:6.46.0:6.46.0 -proto-google-cloud-spanner-v1:6.46.0:6.46.0 -proto-google-cloud-spanner-admin-database-v1:6.46.0:6.46.0 -grpc-google-cloud-spanner-v1:6.46.0:6.46.0 -grpc-google-cloud-spanner-admin-instance-v1:6.46.0:6.46.0 -grpc-google-cloud-spanner-admin-database-v1:6.46.0:6.46.0 -google-cloud-spanner:6.46.0:6.46.0 -google-cloud-spanner-executor:6.46.0:6.46.0 +proto-google-cloud-spanner-admin-instance-v1:6.47.0:6.47.0 +proto-google-cloud-spanner-v1:6.47.0:6.47.0 +proto-google-cloud-spanner-admin-database-v1:6.47.0:6.47.0 +grpc-google-cloud-spanner-v1:6.47.0:6.47.0 +grpc-google-cloud-spanner-admin-instance-v1:6.47.0:6.47.0 +grpc-google-cloud-spanner-admin-database-v1:6.47.0:6.47.0 +google-cloud-spanner:6.47.0:6.47.0 +google-cloud-spanner-executor:6.47.0:6.47.0