Skip to content

Commit b28b660

Browse files
authored
feat: add new Firestore.runAsyncTransaction (#103)
Add the new methods runAsyncTransaction to com.google.cloud.firestore.Firestore following the same pattern as runTransaction however allowing an AsyncFunction to be provided instead of Function.
1 parent 41b2a9a commit b28b660

File tree

4 files changed

+244
-4
lines changed

4 files changed

+244
-4
lines changed

google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,43 @@ <T> ApiFuture<T> runTransaction(
9999
@Nonnull final Transaction.Function<T> updateFunction,
100100
@Nonnull TransactionOptions transactionOptions);
101101

102+
/**
103+
* Executes the given updateFunction and then attempts to commit the changes applied within the
104+
* transaction. If any document read within the transaction has changed, the updateFunction will
105+
* be retried. If it fails to commit after 5 attempts, the transaction will fail. <br>
106+
* <br>
107+
* Running a transaction places locks all consumed documents. To unblock other clients, the
108+
* Firestore backend automatically releases all locks after 60 seconds of inactivity and fails all
109+
* transactions that last longer than 270 seconds (see <a
110+
* href="https://firebase.google.com/docs/firestore/quotas#writes_and_transactions">Firestore
111+
* Quotas</a>).
112+
*
113+
* @param updateFunction The function to execute within the transaction context.
114+
* @return An ApiFuture that will be resolved with the result from updateFunction.
115+
*/
116+
@Nonnull
117+
<T> ApiFuture<T> runAsyncTransaction(@Nonnull final Transaction.AsyncFunction<T> updateFunction);
118+
119+
/**
120+
* Executes the given updateFunction and then attempts to commit the changes applied within the
121+
* transaction. If any document read within the transaction has changed, the updateFunction will
122+
* be retried. If it fails to commit after the maxmimum number of attemps specified in
123+
* transactionOptions, the transaction will fail. <br>
124+
* <br>
125+
* Running a transaction places locks all consumed documents. To unblock other clients, the
126+
* Firestore backend automatically releases all locks after 60 seconds of inactivity and fails all
127+
* transactions that last longer than 270 seconds (see <a
128+
* href="https://firebase.google.com/docs/firestore/quotas#writes_and_transactions">Firestore
129+
* Quotas</a>).
130+
*
131+
* @param updateFunction The function to execute within the transaction context.
132+
* @return An ApiFuture that will be resolved with the result from updateFunction.
133+
*/
134+
@Nonnull
135+
<T> ApiFuture<T> runAsyncTransaction(
136+
@Nonnull final Transaction.AsyncFunction<T> updateFunction,
137+
@Nonnull TransactionOptions transactionOptions);
138+
102139
/**
103140
* Retrieves multiple documents from Firestore.
104141
*

google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,13 +299,30 @@ public <T> ApiFuture<T> runTransaction(
299299
@Nonnull final Transaction.Function<T> updateFunction,
300300
@Nonnull TransactionOptions transactionOptions) {
301301
SettableApiFuture<T> resultFuture = SettableApiFuture.create();
302+
runTransaction(new TransactionAsyncAdapter<>(updateFunction), resultFuture, transactionOptions);
303+
return resultFuture;
304+
}
305+
306+
@Nonnull
307+
@Override
308+
public <T> ApiFuture<T> runAsyncTransaction(
309+
@Nonnull final Transaction.AsyncFunction<T> updateFunction) {
310+
return runAsyncTransaction(updateFunction, TransactionOptions.create());
311+
}
312+
313+
@Nonnull
314+
@Override
315+
public <T> ApiFuture<T> runAsyncTransaction(
316+
@Nonnull final Transaction.AsyncFunction<T> updateFunction,
317+
@Nonnull TransactionOptions transactionOptions) {
318+
SettableApiFuture<T> resultFuture = SettableApiFuture.create();
302319
runTransaction(updateFunction, resultFuture, transactionOptions);
303320
return resultFuture;
304321
}
305322

306323
/** Transaction functions that returns its result in the provided SettableFuture. */
307324
private <T> void runTransaction(
308-
final Transaction.Function<T> transactionCallback,
325+
final Transaction.AsyncFunction<T> transactionCallback,
309326
final SettableApiFuture<T> resultFuture,
310327
final TransactionOptions options) {
311328
// span is intentionally not ended here. It will be ended by runTransactionAttempt on success
@@ -317,7 +334,7 @@ private <T> void runTransaction(
317334
}
318335

319336
private <T> void runTransactionAttempt(
320-
final Transaction.Function<T> transactionCallback,
337+
final Transaction.AsyncFunction<T> transactionCallback,
321338
final SettableApiFuture<T> resultFuture,
322339
final TransactionOptions options,
323340
final Span span) {
@@ -384,7 +401,21 @@ private SettableApiFuture<T> invokeUserCallback() {
384401
@Override
385402
public void run() {
386403
try {
387-
callbackResult.set(transactionCallback.updateCallback(transaction));
404+
ApiFuture<T> updateCallback = transactionCallback.updateCallback(transaction);
405+
ApiFutures.addCallback(
406+
updateCallback,
407+
new ApiFutureCallback<T>() {
408+
@Override
409+
public void onFailure(Throwable t) {
410+
callbackResult.setException(t);
411+
}
412+
413+
@Override
414+
public void onSuccess(T result) {
415+
callbackResult.set(result);
416+
}
417+
},
418+
MoreExecutors.directExecutor());
388419
} catch (Throwable t) {
389420
callbackResult.setException(t);
390421
}
@@ -494,4 +525,23 @@ public void close() throws Exception {
494525
firestoreClient.close();
495526
closed = true;
496527
}
528+
529+
private static class TransactionAsyncAdapter<T> implements Transaction.AsyncFunction<T> {
530+
private final Transaction.Function<T> syncFunction;
531+
532+
public TransactionAsyncAdapter(Transaction.Function<T> syncFunction) {
533+
this.syncFunction = syncFunction;
534+
}
535+
536+
@Override
537+
public ApiFuture<T> updateCallback(Transaction transaction) {
538+
SettableApiFuture<T> callbackResult = SettableApiFuture.create();
539+
try {
540+
callbackResult.set(syncFunction.updateCallback(transaction));
541+
} catch (Throwable e) {
542+
callbackResult.setException(e);
543+
}
544+
return callbackResult;
545+
}
546+
}
497547
}

google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public final class Transaction extends UpdateBuilder<Transaction> {
4242
"Firestore transactions require all reads to be executed before all writes";
4343

4444
/**
45-
* User callback that takes a Firestore Transaction
45+
* User callback that takes a Firestore Transaction.
4646
*
4747
* @param <T> The result type of the user callback.
4848
*/
@@ -51,6 +51,16 @@ public interface Function<T> {
5151
T updateCallback(Transaction transaction) throws Exception;
5252
}
5353

54+
/**
55+
* User callback that takes a Firestore Async Transaction.
56+
*
57+
* @param <T> The result type of the user async callback.
58+
*/
59+
public interface AsyncFunction<T> {
60+
61+
ApiFuture<T> updateCallback(Transaction transaction);
62+
}
63+
5464
private final ByteString previousTransactionId;
5565
private ByteString transactionId;
5666
private boolean pending;

google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import static com.google.cloud.firestore.LocalFirestoreHelper.set;
3535
import static com.google.cloud.firestore.LocalFirestoreHelper.update;
3636
import static org.junit.Assert.assertEquals;
37+
import static org.junit.Assert.assertNull;
3738
import static org.junit.Assert.assertTrue;
3839
import static org.junit.Assert.fail;
3940
import static org.mockito.Mockito.doAnswer;
@@ -132,6 +133,33 @@ public String updateCallback(Transaction transaction) {
132133
assertEquals(commit(TRANSACTION_ID), requests.get(1));
133134
}
134135

136+
@Test
137+
public void returnsValueAsync() throws Exception {
138+
doReturn(beginResponse())
139+
.doReturn(commitResponse(0, 0))
140+
.when(firestoreMock)
141+
.sendRequest(requestCapture.capture(), Matchers.<UnaryCallable<Message, Message>>any());
142+
143+
ApiFuture<String> transaction =
144+
firestoreMock.runAsyncTransaction(
145+
new Transaction.AsyncFunction<String>() {
146+
@Override
147+
public ApiFuture<String> updateCallback(Transaction transaction) {
148+
Assert.assertEquals("user_provided", Thread.currentThread().getName());
149+
return ApiFutures.immediateFuture("foo");
150+
}
151+
},
152+
options);
153+
154+
assertEquals("foo", transaction.get());
155+
156+
List<Message> requests = requestCapture.getAllValues();
157+
assertEquals(2, requests.size());
158+
159+
assertEquals(begin(), requests.get(0));
160+
assertEquals(commit(TRANSACTION_ID), requests.get(1));
161+
}
162+
135163
@Test
136164
public void canReturnNull() throws Exception {
137165
doReturn(beginResponse())
@@ -154,6 +182,28 @@ public String updateCallback(Transaction transaction) {
154182
assertEquals(null, transaction.get());
155183
}
156184

185+
@Test
186+
public void canReturnNullAsync() throws Exception {
187+
doReturn(beginResponse())
188+
.doReturn(ApiFutures.immediateFailedFuture(new Exception()))
189+
.doReturn(beginResponse(ByteString.copyFromUtf8("foo2")))
190+
.doReturn(commitResponse(0, 0))
191+
.when(firestoreMock)
192+
.sendRequest(requestCapture.capture(), Matchers.<UnaryCallable<Message, Message>>any());
193+
194+
ApiFuture<String> transaction =
195+
firestoreMock.runAsyncTransaction(
196+
new Transaction.AsyncFunction<String>() {
197+
@Override
198+
public ApiFuture<String> updateCallback(Transaction transaction) {
199+
return ApiFutures.immediateFuture(null);
200+
}
201+
},
202+
options);
203+
204+
assertNull(transaction.get());
205+
}
206+
157207
@Test
158208
public void rollbackOnCallbackError() throws Exception {
159209
doReturn(beginResponse())
@@ -185,6 +235,37 @@ public String updateCallback(Transaction transaction) throws Exception {
185235
assertEquals(rollback(), requests.get(1));
186236
}
187237

238+
@Test
239+
public void rollbackOnCallbackErrorAsync() throws Exception {
240+
doReturn(beginResponse())
241+
.doReturn(rollbackResponse())
242+
.when(firestoreMock)
243+
.sendRequest(requestCapture.capture(), Matchers.<UnaryCallable<Message, Message>>any());
244+
245+
ApiFuture<String> transaction =
246+
firestoreMock.runAsyncTransaction(
247+
new Transaction.AsyncFunction<String>() {
248+
@Override
249+
public ApiFuture<String> updateCallback(Transaction transaction) {
250+
return ApiFutures.immediateFailedFuture(new Exception("Expected exception"));
251+
}
252+
},
253+
options);
254+
255+
try {
256+
transaction.get();
257+
fail();
258+
} catch (Exception e) {
259+
assertTrue(e.getMessage().endsWith("Expected exception"));
260+
}
261+
262+
List<Message> requests = requestCapture.getAllValues();
263+
assertEquals(2, requests.size());
264+
265+
assertEquals(begin(), requests.get(0));
266+
assertEquals(rollback(), requests.get(1));
267+
}
268+
188269
@Test
189270
public void noRollbackOnBeginFailure() throws Exception {
190271
doReturn(ApiFutures.immediateFailedFuture(new Exception("Expected exception")))
@@ -213,6 +294,34 @@ public String updateCallback(Transaction transaction) {
213294
assertEquals(1, requests.size());
214295
}
215296

297+
@Test
298+
public void noRollbackOnBeginFailureAsync() throws Exception {
299+
doReturn(ApiFutures.immediateFailedFuture(new Exception("Expected exception")))
300+
.when(firestoreMock)
301+
.sendRequest(requestCapture.capture(), Matchers.<UnaryCallable<Message, Message>>any());
302+
303+
ApiFuture<String> transaction =
304+
firestoreMock.runAsyncTransaction(
305+
new Transaction.AsyncFunction<String>() {
306+
@Override
307+
public ApiFuture<String> updateCallback(Transaction transaction) {
308+
fail();
309+
return null;
310+
}
311+
},
312+
options);
313+
314+
try {
315+
transaction.get();
316+
fail();
317+
} catch (Exception e) {
318+
assertTrue(e.getMessage().endsWith("Expected exception"));
319+
}
320+
321+
List<Message> requests = requestCapture.getAllValues();
322+
assertEquals(1, requests.size());
323+
}
324+
216325
@Test
217326
public void limitsRetriesWithFailure() throws Exception {
218327
doReturn(beginResponse(ByteString.copyFromUtf8("foo1")))
@@ -343,6 +452,40 @@ public DocumentSnapshot updateCallback(Transaction transaction)
343452
assertEquals(commit(TRANSACTION_ID), requests.get(2));
344453
}
345454

455+
@Test
456+
public void getDocumentAsync() throws Exception {
457+
doReturn(beginResponse())
458+
.doReturn(commitResponse(0, 0))
459+
.when(firestoreMock)
460+
.sendRequest(requestCapture.capture(), Matchers.<UnaryCallable<Message, Message>>any());
461+
462+
doAnswer(getAllResponse(SINGLE_FIELD_PROTO))
463+
.when(firestoreMock)
464+
.streamRequest(
465+
requestCapture.capture(),
466+
streamObserverCapture.capture(),
467+
Matchers.<ServerStreamingCallable<Message, Message>>any());
468+
469+
ApiFuture<DocumentSnapshot> transaction =
470+
firestoreMock.runAsyncTransaction(
471+
new Transaction.AsyncFunction<DocumentSnapshot>() {
472+
@Override
473+
public ApiFuture<DocumentSnapshot> updateCallback(Transaction transaction) {
474+
return transaction.get(documentReference);
475+
}
476+
},
477+
options);
478+
479+
assertEquals("doc", transaction.get().getId());
480+
481+
List<Message> requests = requestCapture.getAllValues();
482+
assertEquals(3, requests.size());
483+
484+
assertEquals(begin(), requests.get(0));
485+
assertEquals(get(TRANSACTION_ID), requests.get(1));
486+
assertEquals(commit(TRANSACTION_ID), requests.get(2));
487+
}
488+
346489
@Test
347490
public void getMultipleDocuments() throws Exception {
348491
final DocumentReference doc1 = firestoreMock.document("coll/doc1");

0 commit comments

Comments
 (0)