From 76fbae49304291053a245dc2ff1784616576c96c Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:50:14 +0000 Subject: [PATCH 1/7] chore(main): release 6.46.1-SNAPSHOT (#2614) :robot: I have created a release *beep* *boop* --- ### Updating meta-information for bleeding-edge SNAPSHOT release. --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). --- google-cloud-spanner-bom/pom.xml | 20 +++++++++---------- google-cloud-spanner-executor/pom.xml | 4 ++-- google-cloud-spanner/pom.xml | 4 ++-- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- grpc-google-cloud-spanner-v1/pom.xml | 4 ++-- pom.xml | 16 +++++++-------- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- proto-google-cloud-spanner-v1/pom.xml | 4 ++-- samples/snapshot/pom.xml | 2 +- versions.txt | 16 +++++++-------- 12 files changed, 43 insertions(+), 43 deletions(-) diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index f1389078ea2..e9be214c105 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.46.1-SNAPSHOT pom com.google.cloud @@ -53,48 +53,48 @@ com.google.cloud google-cloud-spanner - 6.46.0 + 6.46.1-SNAPSHOT com.google.cloud google-cloud-spanner-executor - 6.46.0 + 6.46.1-SNAPSHOT com.google.cloud google-cloud-spanner test-jar - 6.46.0 + 6.46.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.46.0 + 6.46.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.46.0 + 6.46.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.46.0 + 6.46.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.46.0 + 6.46.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-v1 - 6.46.0 + 6.46.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.46.0 + 6.46.1-SNAPSHOT diff --git a/google-cloud-spanner-executor/pom.xml b/google-cloud-spanner-executor/pom.xml index 7d166579c08..76075b313f5 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.46.1-SNAPSHOT jar Google Cloud Spanner Executor com.google.cloud google-cloud-spanner-parent - 6.46.0 + 6.46.1-SNAPSHOT diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 6b80663eead..7f58cd8744c 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.46.1-SNAPSHOT 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.46.1-SNAPSHOT google-cloud-spanner diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index efbc3323034..bbb279e48ca 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.46.1-SNAPSHOT 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.46.1-SNAPSHOT diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml index 07a676637d4..515885f0c66 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.46.1-SNAPSHOT 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.46.1-SNAPSHOT diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index c87f44d3c78..05c8d91bb30 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.46.1-SNAPSHOT grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.46.0 + 6.46.1-SNAPSHOT diff --git a/pom.xml b/pom.xml index 0274cdc4543..abe1ad2dd53 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 6.46.0 + 6.46.1-SNAPSHOT Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -62,37 +62,37 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.46.0 + 6.46.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-v1 - 6.46.0 + 6.46.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.46.0 + 6.46.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.46.0 + 6.46.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.46.0 + 6.46.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.46.0 + 6.46.1-SNAPSHOT com.google.cloud google-cloud-spanner - 6.46.0 + 6.46.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml index cf0137536c0..6a30cac970d 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.46.1-SNAPSHOT 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.46.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml index fb7fc00a3a3..034ba246375 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.46.1-SNAPSHOT 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.46.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 666edb481bc..8198bcb69f0 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.46.1-SNAPSHOT proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.46.0 + 6.46.1-SNAPSHOT diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index b5e84778899..2aaa9bd57b0 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.46.1-SNAPSHOT diff --git a/versions.txt b/versions.txt index 0b08d5572ee..1f056f87276 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.46.0:6.46.1-SNAPSHOT +proto-google-cloud-spanner-v1:6.46.0:6.46.1-SNAPSHOT +proto-google-cloud-spanner-admin-database-v1:6.46.0:6.46.1-SNAPSHOT +grpc-google-cloud-spanner-v1:6.46.0:6.46.1-SNAPSHOT +grpc-google-cloud-spanner-admin-instance-v1:6.46.0:6.46.1-SNAPSHOT +grpc-google-cloud-spanner-admin-database-v1:6.46.0:6.46.1-SNAPSHOT +google-cloud-spanner:6.46.0:6.46.1-SNAPSHOT +google-cloud-spanner-executor:6.46.0:6.46.1-SNAPSHOT From d855ebbd2dec11cdd6cdbe326de81115632598cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 11 Sep 2023 13:12:58 +0200 Subject: [PATCH 2/7] feat: disable dynamic code loading properties by default (#2606) Disable Connection URL properties that dynamically invoke code by default. These properties can now only be used if the corresponding System property has been enabled. This gives users control over whether they want to enable these properties in their applications or not. You should only enable the use of these properties as long as untrusted end users cannot dynamically set these. That is; If your application is a public service that allows end users to set a Connection URL, then you should not enable the use of these properties, as it would allow a user to try to specify a credentials or channel provider class that is not a valid provider. The constructor of the selected class would in that case still be invoked. --- .../spanner/connection/ConnectionOptions.java | 62 ++++- .../connection/ConnectionOptionsTest.java | 253 ++++++++++++------ .../connection/CredentialsProviderTest.java | 122 +++++---- 3 files changed, 298 insertions(+), 139 deletions(-) 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/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); + } + }); } } From db751ceebc8b6981d00cd07ce4742196cc1dd50d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 11 Sep 2023 14:09:59 +0200 Subject: [PATCH 3/7] fix: avoid unbalanced session pool creation (#2442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: avoid unbalanced session pool creation A query storm at the startup of the client library before the session pool had initialized could cause the creation of an unbalanced session pool. This again would put a large batch of sessions using the same gRPC channel at the head of the pool, which could then continously be used by the application. * fix: automatically balance pool * fix: skip empty pool * fix: shuffle if unbalanced * fix: only reset randomness if actually randomized * test: randomize if many sessions are checked out * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * test: try with more channels * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: also consider checked out sessions for unbalanced pool * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * docs: add javadoc for property * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * perf: optimize low-QPS workloads * test: only randomize if more than 2 sessions are checked out * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * test: only skip randomization for existing sessions * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * chore: run formatter * chore: address review comments * docs: update comment on how session is added to the pool --------- Co-authored-by: Owl Bot --- .../com/google/cloud/spanner/SessionPool.java | 170 ++++++++++-- .../cloud/spanner/BaseSessionPoolTest.java | 8 + .../spanner/SessionPoolMaintainerTest.java | 6 +- .../cloud/spanner/SessionPoolStressTest.java | 29 ++- .../google/cloud/spanner/SessionPoolTest.java | 20 +- .../spanner/SessionPoolUnbalancedTest.java | 241 ++++++++++++++++++ 6 files changed, 439 insertions(+), 35 deletions(-) create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolUnbalancedTest.java 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/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)); + } +} From c42460ae7b6bb5874cc18c7aecff34186dcbff2a Mon Sep 17 00:00:00 2001 From: Mridula <66699525+mpeddada1@users.noreply.github.com> Date: Tue, 12 Sep 2023 08:54:14 +0000 Subject: [PATCH 4/7] fix: add reflection configurations for com.google.rpc classes (#2617) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add native image configurations for com.google.rpc classes * relocate config to separate reflect-config.json files * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- README.md | 6 +- .../reflect-config.json | 172 ++++++++++++++++++ 2 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 google-cloud-spanner/src/main/resources/META-INF/native-image/com.google.cloud.spanner/reflect-config.json 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/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 From ac762fbf079db79eab5f2ebee971b850ac89eb11 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 12 Sep 2023 11:34:38 +0200 Subject: [PATCH 5/7] deps: update dependency com.google.cloud:google-cloud-shared-dependencies to v3.15.0 (#2615) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [com.google.cloud:google-cloud-shared-dependencies](https://togithub.com/googleapis/sdk-platform-java) | `3.14.0` -> `3.15.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.cloud:google-cloud-shared-dependencies/3.15.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.cloud:google-cloud-shared-dependencies/3.15.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.cloud:google-cloud-shared-dependencies/3.14.0/3.15.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.cloud:google-cloud-shared-dependencies/3.14.0/3.15.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/java-spanner). --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index abe1ad2dd53..6b6421aa019 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ UTF-8 github google-cloud-spanner-parent - 3.14.0 + 3.15.0
From a7d60f13781f87054a1631ca511492c5c8334751 Mon Sep 17 00:00:00 2001 From: Arpan Mishra Date: Tue, 12 Sep 2023 17:20:28 +0530 Subject: [PATCH 6/7] feat: add devcontainers for enabling github codespaces usage. (#2605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add devcontainers for enabling github codespaces usage. * fix: enable intallation of maven. * fix: add postCreateCommand script. * fix:upgrade to Java 11 to fix build errors in container builds. * chore: add 'clean' in the postCreate script. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: review comments. --------- Co-authored-by: Owl Bot --- .devcontainer/Dockerfile | 21 ++++++++++++++++ .devcontainer/devcontainer.json | 43 +++++++++++++++++++++++++++++++++ .devcontainer/postCreate.sh | 6 +++++ 3 files changed, 70 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/postCreate.sh 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 From 970bd4502c69c8d500813bac7a3a309e50787eec Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 12 Sep 2023 12:58:13 +0000 Subject: [PATCH 7/7] chore(main): release 6.47.0 (#2619) :robot: I have created a release *beep* *boop* --- ## [6.47.0](https://togithub.com/googleapis/java-spanner/compare/v6.46.0...v6.47.0) (2023-09-12) ### Features * Add devcontainers for enabling github codespaces usage. ([#2605](https://togithub.com/googleapis/java-spanner/issues/2605)) ([a7d60f1](https://togithub.com/googleapis/java-spanner/commit/a7d60f13781f87054a1631ca511492c5c8334751)) * Disable dynamic code loading properties by default ([#2606](https://togithub.com/googleapis/java-spanner/issues/2606)) ([d855ebb](https://togithub.com/googleapis/java-spanner/commit/d855ebbd2dec11cdd6cdbe326de81115632598cd)) ### Bug Fixes * Add reflection configurations for com.google.rpc classes ([#2617](https://togithub.com/googleapis/java-spanner/issues/2617)) ([c42460a](https://togithub.com/googleapis/java-spanner/commit/c42460ae7b6bb5874cc18c7aecff34186dcbff2a)) * Avoid unbalanced session pool creation ([#2442](https://togithub.com/googleapis/java-spanner/issues/2442)) ([db751ce](https://togithub.com/googleapis/java-spanner/commit/db751ceebc8b6981d00cd07ce4742196cc1dd50d)) ### Dependencies * Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.15.0 ([#2615](https://togithub.com/googleapis/java-spanner/issues/2615)) ([ac762fb](https://togithub.com/googleapis/java-spanner/commit/ac762fbf079db79eab5f2ebee971b850ac89eb11)) --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). --- CHANGELOG.md | 19 ++++++++++++++++++ google-cloud-spanner-bom/pom.xml | 20 +++++++++---------- google-cloud-spanner-executor/pom.xml | 4 ++-- google-cloud-spanner/pom.xml | 4 ++-- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- grpc-google-cloud-spanner-v1/pom.xml | 4 ++-- pom.xml | 16 +++++++-------- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- proto-google-cloud-spanner-v1/pom.xml | 4 ++-- samples/snapshot/pom.xml | 2 +- versions.txt | 16 +++++++-------- 13 files changed, 62 insertions(+), 43 deletions(-) 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/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index e9be214c105..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.1-SNAPSHOT + 6.47.0 pom com.google.cloud @@ -53,48 +53,48 @@ com.google.cloud google-cloud-spanner - 6.46.1-SNAPSHOT + 6.47.0 com.google.cloud google-cloud-spanner-executor - 6.46.1-SNAPSHOT + 6.47.0 com.google.cloud google-cloud-spanner test-jar - 6.46.1-SNAPSHOT + 6.47.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.46.1-SNAPSHOT + 6.47.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.46.1-SNAPSHOT + 6.47.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.46.1-SNAPSHOT + 6.47.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.46.1-SNAPSHOT + 6.47.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.46.1-SNAPSHOT + 6.47.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.46.1-SNAPSHOT + 6.47.0 diff --git a/google-cloud-spanner-executor/pom.xml b/google-cloud-spanner-executor/pom.xml index 76075b313f5..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.1-SNAPSHOT + 6.47.0 jar Google Cloud Spanner Executor com.google.cloud google-cloud-spanner-parent - 6.46.1-SNAPSHOT + 6.47.0 diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 7f58cd8744c..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.1-SNAPSHOT + 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.1-SNAPSHOT + 6.47.0 google-cloud-spanner diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index bbb279e48ca..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.1-SNAPSHOT + 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.1-SNAPSHOT + 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 515885f0c66..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.1-SNAPSHOT + 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.1-SNAPSHOT + 6.47.0 diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index 05c8d91bb30..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.1-SNAPSHOT + 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.1-SNAPSHOT + 6.47.0 diff --git a/pom.xml b/pom.xml index 6b6421aa019..017960333a8 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 6.46.1-SNAPSHOT + 6.47.0 Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -62,37 +62,37 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.46.1-SNAPSHOT + 6.47.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.46.1-SNAPSHOT + 6.47.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.46.1-SNAPSHOT + 6.47.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.46.1-SNAPSHOT + 6.47.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.46.1-SNAPSHOT + 6.47.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.46.1-SNAPSHOT + 6.47.0 com.google.cloud google-cloud-spanner - 6.46.1-SNAPSHOT + 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 6a30cac970d..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.1-SNAPSHOT + 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.1-SNAPSHOT + 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 034ba246375..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.1-SNAPSHOT + 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.1-SNAPSHOT + 6.47.0 diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 8198bcb69f0..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.1-SNAPSHOT + 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.1-SNAPSHOT + 6.47.0 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 2aaa9bd57b0..faddfc846e0 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -31,7 +31,7 @@ com.google.cloud google-cloud-spanner - 6.46.1-SNAPSHOT + 6.47.0 diff --git a/versions.txt b/versions.txt index 1f056f87276..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.1-SNAPSHOT -proto-google-cloud-spanner-v1:6.46.0:6.46.1-SNAPSHOT -proto-google-cloud-spanner-admin-database-v1:6.46.0:6.46.1-SNAPSHOT -grpc-google-cloud-spanner-v1:6.46.0:6.46.1-SNAPSHOT -grpc-google-cloud-spanner-admin-instance-v1:6.46.0:6.46.1-SNAPSHOT -grpc-google-cloud-spanner-admin-database-v1:6.46.0:6.46.1-SNAPSHOT -google-cloud-spanner:6.46.0:6.46.1-SNAPSHOT -google-cloud-spanner-executor:6.46.0:6.46.1-SNAPSHOT +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