diff --git a/README.md b/README.md index 31a90d7fb..ff9613efd 100644 --- a/README.md +++ b/README.md @@ -50,20 +50,20 @@ If you are using Maven without the BOM, add this to your dependencies: If you are using Gradle 5.x or later, add this to your dependencies: ```Groovy -implementation platform('com.google.cloud:libraries-bom:26.27.0') +implementation platform('com.google.cloud:libraries-bom:26.29.0') implementation 'com.google.cloud:google-cloud-datastore' ``` If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-datastore:2.17.5' +implementation 'com.google.cloud:google-cloud-datastore:2.17.6' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-datastore" % "2.17.5" +libraryDependencies += "com.google.cloud" % "google-cloud-datastore" % "2.17.6" ``` @@ -380,7 +380,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-datastore/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-datastore.svg -[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-datastore/2.17.5 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-datastore/2.17.6 [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-datastore/clirr-ignored-differences.xml b/google-cloud-datastore/clirr-ignored-differences.xml index 62459c953..f80562fba 100644 --- a/google-cloud-datastore/clirr-ignored-differences.xml +++ b/google-cloud-datastore/clirr-ignored-differences.xml @@ -38,4 +38,25 @@ 5001 com/google/cloud/http/BaseHttpServiceException + + com/google/cloud/datastore/Datastore + void close() + 7012 + + + com/google/cloud/datastore/spi/v1/DatastoreRpc + void close() + 7012 + + + com/google/cloud/datastore/Datastore + boolean isClosed() + 7012 + + + com/google/cloud/datastore/spi/v1/DatastoreRpc + boolean isClosed() + 7012 + + diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Datastore.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Datastore.java index d78bea9a2..1fb5fcedc 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Datastore.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Datastore.java @@ -22,7 +22,7 @@ import java.util.List; /** An interface for Google Cloud Datastore. */ -public interface Datastore extends Service, DatastoreReaderWriter { +public interface Datastore extends Service, DatastoreReaderWriter, AutoCloseable { /** * Returns a new Datastore transaction. @@ -49,9 +49,9 @@ public interface Datastore extends Service, DatastoreReaderWri * @param the type of the return value */ interface TransactionCallable { + T run(DatastoreReaderWriter readerWriter) throws Exception; } - /** * Invokes the callback's {@link Datastore.TransactionCallable#run} method with a {@link * DatastoreReaderWriter} that is associated with a new transaction. The transaction will be @@ -508,4 +508,15 @@ interface TransactionCallable { default AggregationResults runAggregation(AggregationQuery query, ReadOption... options) { throw new UnsupportedOperationException("Not implemented."); } + + /** + * Closes the gRPC channels associated with this instance and frees up their resources. This + * method blocks until all channels are closed. Once this method is called, this Datastore client + * is no longer usable. + */ + @Override + void close() throws Exception; + + /** Returns true if this background resource has been shut down. */ + boolean isClosed(); } diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreImpl.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreImpl.java index a1b337c05..55add1c9f 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreImpl.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreImpl.java @@ -48,9 +48,12 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; +import java.util.logging.Level; +import java.util.logging.Logger; final class DatastoreImpl extends BaseService implements Datastore { + Logger logger = Logger.getLogger(Datastore.class.getName()); private final DatastoreRpc datastoreRpc; private final RetrySettings retrySettings; private static final ExceptionHandler TRANSACTION_EXCEPTION_HANDLER = @@ -90,6 +93,20 @@ public Transaction newTransaction() { return new TransactionImpl(this); } + @Override + public void close() throws Exception { + try { + datastoreRpc.close(); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to close channels", e); + } + } + + @Override + public boolean isClosed() { + return datastoreRpc.isClosed(); + } + static class ReadWriteTransactionCallable implements Callable { private final Datastore datastore; diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/RetryAndTraceDatastoreRpcDecorator.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/RetryAndTraceDatastoreRpcDecorator.java index c4a85caab..920fb440f 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/RetryAndTraceDatastoreRpcDecorator.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/RetryAndTraceDatastoreRpcDecorator.java @@ -109,6 +109,16 @@ public RunAggregationQueryResponse runAggregationQuery(RunAggregationQueryReques () -> datastoreRpc.runAggregationQuery(request), SPAN_NAME_RUN_AGGREGATION_QUERY); } + @Override + public void close() throws Exception { + datastoreRpc.close(); + } + + @Override + public boolean isClosed() { + return datastoreRpc.isClosed(); + } + public O invokeRpc(Callable block, String startSpan) { Span span = traceUtil.startSpan(startSpan); try (Scope scope = traceUtil.getTracer().withSpan(span)) { diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/spi/v1/DatastoreRpc.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/spi/v1/DatastoreRpc.java index 33b8e11ea..24b5b0166 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/spi/v1/DatastoreRpc.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/spi/v1/DatastoreRpc.java @@ -36,7 +36,7 @@ import com.google.datastore.v1.RunQueryResponse; /** Provides access to the remote Datastore service. */ -public interface DatastoreRpc extends ServiceRpc { +public interface DatastoreRpc extends ServiceRpc, AutoCloseable { /** * Sends an allocate IDs request. @@ -96,4 +96,10 @@ BeginTransactionResponse beginTransaction(BeginTransactionRequest request) default RunAggregationQueryResponse runAggregationQuery(RunAggregationQueryRequest request) { throw new UnsupportedOperationException("Not implemented."); } + + @Override + void close() throws Exception; + + /** Returns true if this background resource has been shut down. */ + boolean isClosed(); } diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/spi/v1/GrpcDatastoreRpc.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/spi/v1/GrpcDatastoreRpc.java index b4c83da89..542d16d31 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/spi/v1/GrpcDatastoreRpc.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/spi/v1/GrpcDatastoreRpc.java @@ -63,7 +63,7 @@ import java.util.Collections; @InternalApi -public class GrpcDatastoreRpc implements AutoCloseable, DatastoreRpc { +public class GrpcDatastoreRpc implements DatastoreRpc { private final GrpcDatastoreStub datastoreStub; private final ClientContext clientContext; @@ -146,6 +146,11 @@ public RunAggregationQueryResponse runAggregationQuery(RunAggregationQueryReques return datastoreStub.runAggregationQueryCallable().call(request); } + @Override + public boolean isClosed() { + return closed && datastoreStub.isShutdown(); + } + private boolean isEmulator(DatastoreOptions datastoreOptions) { return isLocalHost(datastoreOptions.getHost()) || NoCredentials.getInstance().equals(datastoreOptions.getCredentials()); diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/spi/v1/HttpDatastoreRpc.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/spi/v1/HttpDatastoreRpc.java index fd3cdc658..66bb0497b 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/spi/v1/HttpDatastoreRpc.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/spi/v1/HttpDatastoreRpc.java @@ -211,4 +211,14 @@ public RunAggregationQueryResponse runAggregationQuery(RunAggregationQueryReques throw translate(ex); } } + + @Override + public void close() throws Exception { + throw new UnsupportedOperationException("close() is not supported"); + } + + @Override + public boolean isClosed() { + throw new UnsupportedOperationException("isClosed() is not supported"); + } } diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/testing/LocalDatastoreHelper.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/testing/LocalDatastoreHelper.java index 26b892186..927a6cf23 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/testing/LocalDatastoreHelper.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/testing/LocalDatastoreHelper.java @@ -57,12 +57,12 @@ public class LocalDatastoreHelper extends BaseEmulatorHelper { private static final String GCLOUD_CMD_TEXT = "gcloud beta emulators datastore start"; private static final String GCLOUD_CMD_PORT_FLAG = "--host-port="; private static final String VERSION_PREFIX = "cloud-datastore-emulator "; - private static final String MIN_VERSION = "1.2.0"; + private static final String MIN_VERSION = "2.0.2"; // latest version compatible with java 8 // Downloadable emulator settings private static final String BIN_NAME = "cloud-datastore-emulator/cloud_datastore_emulator"; private static final String FILENAME = "cloud-datastore-emulator-" + MIN_VERSION + ".zip"; - private static final String MD5_CHECKSUM = "ec2237a0f0ac54964c6bd95e12c73720"; + private static final String MD5_CHECKSUM = "e0d1170519cf52e2e5f9f93892cdf70c"; private static final String BIN_CMD_PORT_FLAG = "--port="; private static final URL EMULATOR_URL; private static final String EMULATOR_URL_ENV_VAR = "DATASTORE_EMULATOR_URL"; diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/DatastoreTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/DatastoreTest.java index 5f42e2aeb..84cbd4b73 100644 --- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/DatastoreTest.java +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/DatastoreTest.java @@ -31,6 +31,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -39,7 +40,6 @@ import com.google.cloud.datastore.Query.ResultType; import com.google.cloud.datastore.StructuredQuery.OrderBy; import com.google.cloud.datastore.StructuredQuery.PropertyFilter; -import com.google.cloud.datastore.it.MultipleAttemptsRule; import com.google.cloud.datastore.spi.DatastoreRpcFactory; import com.google.cloud.datastore.spi.v1.DatastoreRpc; import com.google.cloud.datastore.testing.LocalDatastoreHelper; @@ -79,14 +79,12 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeoutException; import java.util.function.Predicate; import org.easymock.EasyMock; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -94,14 +92,9 @@ @RunWith(JUnit4.class) public class DatastoreTest { - private static final int NUMBER_OF_ATTEMPTS = 5; - - @ClassRule - public static MultipleAttemptsRule rr = new MultipleAttemptsRule(NUMBER_OF_ATTEMPTS, 10); - - private static LocalDatastoreHelper helper = LocalDatastoreHelper.create(1.0); - private static final DatastoreOptions options = helper.getOptions(); - private static final Datastore datastore = options.getService(); + private static final LocalDatastoreHelper helper = LocalDatastoreHelper.create(1.0, 9090); + private static DatastoreOptions options = helper.getOptions(); + private static Datastore datastore; private static final String PROJECT_ID = options.getProjectId(); private static final String KIND1 = "kind1"; private static final String KIND2 = "kind2"; @@ -177,6 +170,8 @@ public class DatastoreTest { @BeforeClass public static void beforeClass() throws IOException, InterruptedException { helper.start(); + options = helper.getOptions(); + datastore = options.getService(); } @Before @@ -197,7 +192,8 @@ public void setUp() { } @AfterClass - public static void afterClass() throws IOException, InterruptedException, TimeoutException { + public static void afterClass() throws Exception { + datastore.close(); helper.stop(Duration.ofMinutes(1)); } @@ -1386,6 +1382,21 @@ public void testDatabaseIdKeyFactory() { checkKeyProperties(incompleteKey); } + @Test + public void testDatastoreClose() throws Exception { + Datastore datastore = options.toBuilder().build().getService(); + Entity entity = datastore.get(KEY3); + assertNull(entity); + + datastore.close(); + assertTrue(datastore.isClosed()); + + assertThrows( + "io.grpc.StatusRuntimeException: UNAVAILABLE: Channel shutdown invoked", + DatastoreException.class, + () -> datastore.get(KEY3)); + } + private void checkKeyProperties(BaseKey key) { assertEquals(options.getDatabaseId(), key.getDatabaseId()); assertEquals(options.getProjectId(), key.getProjectId()); diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreAggregationsTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreAggregationsTest.java index fd430095f..e04f5e55f 100644 --- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreAggregationsTest.java +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreAggregationsTest.java @@ -43,6 +43,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import org.junit.After; +import org.junit.AfterClass; import org.junit.Test; // TODO(jainsahab) Move all the aggregation related tests from ITDatastoreTest to this file @@ -63,6 +64,11 @@ public void tearDown() { DATASTORE.delete(keysToDelete); } + @AfterClass + public static void afterClass() throws Exception { + DATASTORE.close(); + } + Key key1 = DATASTORE.newKeyFactory().setKind(KIND).newKey(1); Key key2 = DATASTORE.newKeyFactory().setKind(KIND).newKey(2); Key key3 = DATASTORE.newKeyFactory().setKind(KIND).newKey(3); diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreConceptsTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreConceptsTest.java index b8ebd277a..f61db4f48 100644 --- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreConceptsTest.java +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreConceptsTest.java @@ -67,7 +67,9 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; /* @@ -77,7 +79,7 @@ public class ITDatastoreConceptsTest { private static final RemoteDatastoreHelper HELPER = RemoteDatastoreHelper.create(); private static final DatastoreOptions OPTIONS = HELPER.getOptions(); private static final FullEntity TEST_FULL_ENTITY = FullEntity.newBuilder().build(); - private Datastore datastore; + private static Datastore datastore; private KeyFactory keyFactory; private Key taskKey; private Entity testEntity; @@ -87,13 +89,15 @@ public class ITDatastoreConceptsTest { private static final String TASK_CONCEPTS = "TaskConcepts"; - /** - * Initializes Datastore and cleans out any residual values. Also initializes global variables - * used for testing. - */ + /** Initializes Datastore for testing. */ + @BeforeClass + public static void beforeClass() throws Exception { + datastore = OPTIONS.getService(); + } + + /** Cleans out any residual values. Also initializes global variables used for testing. */ @Before public void setUp() { - datastore = OPTIONS.getService(); StructuredQuery query = Query.newKeyQueryBuilder().build(); QueryResults result = datastore.run(query); datastore.delete(Iterators.toArray(result, Key.class)); @@ -128,6 +132,11 @@ public void tearDown() { datastore.delete(taskKeysToDelete); } + @AfterClass + public static void afterClass() throws Exception { + datastore.close(); + } + private void assertValidKey(Key taskKey) { datastore.put(Entity.newBuilder(taskKey, TEST_FULL_ENTITY).build()); } diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java index 1e931dfc4..3f00fe2cf 100644 --- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java @@ -144,8 +144,10 @@ public class ITDatastoreTest { @Rule public MultipleAttemptsRule multipleAttemptsRule = new MultipleAttemptsRule(3); @AfterClass - public static void afterClass() { + public static void afterClass() throws Exception { HELPER.deleteNamespace(); + DATASTORE_1.close(); + DATASTORE_2.close(); } public ITDatastoreTest(