diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java index 26ee155ea..8c0301b3d 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java @@ -99,6 +99,43 @@ ApiFuture runTransaction( @Nonnull final Transaction.Function updateFunction, @Nonnull TransactionOptions transactionOptions); + /** + * Executes the given updateFunction and then attempts to commit the changes applied within the + * transaction. If any document read within the transaction has changed, the updateFunction will + * be retried. If it fails to commit after 5 attempts, the transaction will fail.
+ *
+ * Running a transaction places locks all consumed documents. To unblock other clients, the + * Firestore backend automatically releases all locks after 60 seconds of inactivity and fails all + * transactions that last longer than 270 seconds (see Firestore + * Quotas). + * + * @param updateFunction The function to execute within the transaction context. + * @return An ApiFuture that will be resolved with the result from updateFunction. + */ + @Nonnull + ApiFuture runAsyncTransaction(@Nonnull final Transaction.AsyncFunction updateFunction); + + /** + * Executes the given updateFunction and then attempts to commit the changes applied within the + * transaction. If any document read within the transaction has changed, the updateFunction will + * be retried. If it fails to commit after the maxmimum number of attemps specified in + * transactionOptions, the transaction will fail.
+ *
+ * Running a transaction places locks all consumed documents. To unblock other clients, the + * Firestore backend automatically releases all locks after 60 seconds of inactivity and fails all + * transactions that last longer than 270 seconds (see Firestore + * Quotas). + * + * @param updateFunction The function to execute within the transaction context. + * @return An ApiFuture that will be resolved with the result from updateFunction. + */ + @Nonnull + ApiFuture runAsyncTransaction( + @Nonnull final Transaction.AsyncFunction updateFunction, + @Nonnull TransactionOptions transactionOptions); + /** * Retrieves multiple documents from Firestore. * diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java index f70f28c9d..830ae482f 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java @@ -299,13 +299,30 @@ public ApiFuture runTransaction( @Nonnull final Transaction.Function updateFunction, @Nonnull TransactionOptions transactionOptions) { SettableApiFuture resultFuture = SettableApiFuture.create(); + runTransaction(new TransactionAsyncAdapter<>(updateFunction), resultFuture, transactionOptions); + return resultFuture; + } + + @Nonnull + @Override + public ApiFuture runAsyncTransaction( + @Nonnull final Transaction.AsyncFunction updateFunction) { + return runAsyncTransaction(updateFunction, TransactionOptions.create()); + } + + @Nonnull + @Override + public ApiFuture runAsyncTransaction( + @Nonnull final Transaction.AsyncFunction updateFunction, + @Nonnull TransactionOptions transactionOptions) { + SettableApiFuture resultFuture = SettableApiFuture.create(); runTransaction(updateFunction, resultFuture, transactionOptions); return resultFuture; } /** Transaction functions that returns its result in the provided SettableFuture. */ private void runTransaction( - final Transaction.Function transactionCallback, + final Transaction.AsyncFunction transactionCallback, final SettableApiFuture resultFuture, final TransactionOptions options) { // span is intentionally not ended here. It will be ended by runTransactionAttempt on success @@ -317,7 +334,7 @@ private void runTransaction( } private void runTransactionAttempt( - final Transaction.Function transactionCallback, + final Transaction.AsyncFunction transactionCallback, final SettableApiFuture resultFuture, final TransactionOptions options, final Span span) { @@ -384,7 +401,21 @@ private SettableApiFuture invokeUserCallback() { @Override public void run() { try { - callbackResult.set(transactionCallback.updateCallback(transaction)); + ApiFuture updateCallback = transactionCallback.updateCallback(transaction); + ApiFutures.addCallback( + updateCallback, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + callbackResult.setException(t); + } + + @Override + public void onSuccess(T result) { + callbackResult.set(result); + } + }, + MoreExecutors.directExecutor()); } catch (Throwable t) { callbackResult.setException(t); } @@ -494,4 +525,23 @@ public void close() throws Exception { firestoreClient.close(); closed = true; } + + private static class TransactionAsyncAdapter implements Transaction.AsyncFunction { + private final Transaction.Function syncFunction; + + public TransactionAsyncAdapter(Transaction.Function syncFunction) { + this.syncFunction = syncFunction; + } + + @Override + public ApiFuture updateCallback(Transaction transaction) { + SettableApiFuture callbackResult = SettableApiFuture.create(); + try { + callbackResult.set(syncFunction.updateCallback(transaction)); + } catch (Throwable e) { + callbackResult.setException(e); + } + return callbackResult; + } + } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java index 13f51c0c7..e6c03b470 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java @@ -42,7 +42,7 @@ public final class Transaction extends UpdateBuilder { "Firestore transactions require all reads to be executed before all writes"; /** - * User callback that takes a Firestore Transaction + * User callback that takes a Firestore Transaction. * * @param The result type of the user callback. */ @@ -51,6 +51,16 @@ public interface Function { T updateCallback(Transaction transaction) throws Exception; } + /** + * User callback that takes a Firestore Async Transaction. + * + * @param The result type of the user async callback. + */ + public interface AsyncFunction { + + ApiFuture updateCallback(Transaction transaction); + } + private final ByteString previousTransactionId; private ByteString transactionId; private boolean pending; diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java index 8895bca4f..989d815e3 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java @@ -34,6 +34,7 @@ import static com.google.cloud.firestore.LocalFirestoreHelper.set; import static com.google.cloud.firestore.LocalFirestoreHelper.update; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.doAnswer; @@ -132,6 +133,33 @@ public String updateCallback(Transaction transaction) { assertEquals(commit(TRANSACTION_ID), requests.get(1)); } + @Test + public void returnsValueAsync() throws Exception { + doReturn(beginResponse()) + .doReturn(commitResponse(0, 0)) + .when(firestoreMock) + .sendRequest(requestCapture.capture(), Matchers.>any()); + + ApiFuture transaction = + firestoreMock.runAsyncTransaction( + new Transaction.AsyncFunction() { + @Override + public ApiFuture updateCallback(Transaction transaction) { + Assert.assertEquals("user_provided", Thread.currentThread().getName()); + return ApiFutures.immediateFuture("foo"); + } + }, + options); + + assertEquals("foo", transaction.get()); + + List requests = requestCapture.getAllValues(); + assertEquals(2, requests.size()); + + assertEquals(begin(), requests.get(0)); + assertEquals(commit(TRANSACTION_ID), requests.get(1)); + } + @Test public void canReturnNull() throws Exception { doReturn(beginResponse()) @@ -154,6 +182,28 @@ public String updateCallback(Transaction transaction) { assertEquals(null, transaction.get()); } + @Test + public void canReturnNullAsync() throws Exception { + doReturn(beginResponse()) + .doReturn(ApiFutures.immediateFailedFuture(new Exception())) + .doReturn(beginResponse(ByteString.copyFromUtf8("foo2"))) + .doReturn(commitResponse(0, 0)) + .when(firestoreMock) + .sendRequest(requestCapture.capture(), Matchers.>any()); + + ApiFuture transaction = + firestoreMock.runAsyncTransaction( + new Transaction.AsyncFunction() { + @Override + public ApiFuture updateCallback(Transaction transaction) { + return ApiFutures.immediateFuture(null); + } + }, + options); + + assertNull(transaction.get()); + } + @Test public void rollbackOnCallbackError() throws Exception { doReturn(beginResponse()) @@ -185,6 +235,37 @@ public String updateCallback(Transaction transaction) throws Exception { assertEquals(rollback(), requests.get(1)); } + @Test + public void rollbackOnCallbackErrorAsync() throws Exception { + doReturn(beginResponse()) + .doReturn(rollbackResponse()) + .when(firestoreMock) + .sendRequest(requestCapture.capture(), Matchers.>any()); + + ApiFuture transaction = + firestoreMock.runAsyncTransaction( + new Transaction.AsyncFunction() { + @Override + public ApiFuture updateCallback(Transaction transaction) { + return ApiFutures.immediateFailedFuture(new Exception("Expected exception")); + } + }, + options); + + try { + transaction.get(); + fail(); + } catch (Exception e) { + assertTrue(e.getMessage().endsWith("Expected exception")); + } + + List requests = requestCapture.getAllValues(); + assertEquals(2, requests.size()); + + assertEquals(begin(), requests.get(0)); + assertEquals(rollback(), requests.get(1)); + } + @Test public void noRollbackOnBeginFailure() throws Exception { doReturn(ApiFutures.immediateFailedFuture(new Exception("Expected exception"))) @@ -213,6 +294,34 @@ public String updateCallback(Transaction transaction) { assertEquals(1, requests.size()); } + @Test + public void noRollbackOnBeginFailureAsync() throws Exception { + doReturn(ApiFutures.immediateFailedFuture(new Exception("Expected exception"))) + .when(firestoreMock) + .sendRequest(requestCapture.capture(), Matchers.>any()); + + ApiFuture transaction = + firestoreMock.runAsyncTransaction( + new Transaction.AsyncFunction() { + @Override + public ApiFuture updateCallback(Transaction transaction) { + fail(); + return null; + } + }, + options); + + try { + transaction.get(); + fail(); + } catch (Exception e) { + assertTrue(e.getMessage().endsWith("Expected exception")); + } + + List requests = requestCapture.getAllValues(); + assertEquals(1, requests.size()); + } + @Test public void limitsRetriesWithFailure() throws Exception { doReturn(beginResponse(ByteString.copyFromUtf8("foo1"))) @@ -343,6 +452,40 @@ public DocumentSnapshot updateCallback(Transaction transaction) assertEquals(commit(TRANSACTION_ID), requests.get(2)); } + @Test + public void getDocumentAsync() throws Exception { + doReturn(beginResponse()) + .doReturn(commitResponse(0, 0)) + .when(firestoreMock) + .sendRequest(requestCapture.capture(), Matchers.>any()); + + doAnswer(getAllResponse(SINGLE_FIELD_PROTO)) + .when(firestoreMock) + .streamRequest( + requestCapture.capture(), + streamObserverCapture.capture(), + Matchers.>any()); + + ApiFuture transaction = + firestoreMock.runAsyncTransaction( + new Transaction.AsyncFunction() { + @Override + public ApiFuture updateCallback(Transaction transaction) { + return transaction.get(documentReference); + } + }, + options); + + assertEquals("doc", transaction.get().getId()); + + List requests = requestCapture.getAllValues(); + assertEquals(3, requests.size()); + + assertEquals(begin(), requests.get(0)); + assertEquals(get(TRANSACTION_ID), requests.get(1)); + assertEquals(commit(TRANSACTION_ID), requests.get(2)); + } + @Test public void getMultipleDocuments() throws Exception { final DocumentReference doc1 = firestoreMock.document("coll/doc1");