Does firebase transaction retry run codes beyond the transaction? - firebase

If there are more codes/logic outside the transaction that should only be run when the transaction succeeds, will these codes be ran after the retry succeed? See construed example below based on my Express route handler.
app.post('/some/path/to/endpoint', async (req, res) => {
try {
await db.runTransaction(async t => {
const snapshot = t.get(someDocRef);
const data = snapshot.data();
doSomething(snapshot);
return t.update(snapshot.ref, { someChanges });
});
// QUESTION: If transaction retries and succeeds, will the below code run once?
// logic that requires the transaction succeeds
await axios.post(url, data);
res.status(200).send('success');
} catch (e) {
res.status(500).send('system error');
}
});
Appreciate expert views on this. Thanks

You can find the documentation for runTransaction here.
As you can see, runTransaction() returns a Promise. When you await a Promise, and with you code inserted in a try/catch block, if an error is thrown everything after will be ignored, because the flow will go in the catch statement.
So the answer is yes: everything after runTransaction(), the way you wrote it, will not be executed if something goes wrong.

Related

How to unit-test timeout in Flutter?

I have a facade function that reloads the current firebase user and returns it. The thing is that the user reloading part has a timeout and it needs to be tested.
Function:
Future<Option<User>> getSignedInUser() async {
// Reload currentUser if possible
// it mustn't throw [TimeoutException] for whole function,
// this is what this try/catch does
try {
await reloadCurrentUser().timeout(const Duration(seconds: 20));
} catch (e) {
log(e.toString(), name: TAG);
}
return optionOf(_auth.currentUser);
}
reloadCurrentUser() function:
Future<Either<AuthFailure, Unit>> reloadCurrentUser() async {
try {
await _auth.currentUser?.reload();
return right(unit);
} catch (e) {
log(e.toString(), name: TAG);
return left(const AuthFailure.userReloadingError());
}
}
The question is how to test reloadCurrentUser() timeout? I'm trying to throw a TimeoutException when this function is called, but then it throws an error for the whole test.
Current Test function:
test(
'Reaches timeout when reloading currentUser, '
'throws TimeoutException, but function continues '
'and returns optionOf currentUser', () async {
reset(fakeFirebaseAuth);
reset(fakeFacebookAuth);
reset(fakeGoogleSignIn);
final currentUser = FakeUser();
// It says that currentUser exists and *IS* authenticated
when(() => fakeFirebaseAuth.currentUser).thenReturn(currentUser);
when(() => firebaseAuthFacade.reloadCurrentUser())
.thenThrow(TimeoutException('timeout', const Duration(seconds: 20)));
final result = await firebaseAuthFacade.getSignedInUser();
expect(result, isA<Some<User>>());
});
Maybe it's better to remove timeout and use some connectivity package to ensure that the user has a network connection and only then reload the current user?
For testing I'm using mocktail package.
You can use the fake_async package.
Here's a simple example from their docs that you can modify for your use case:
import 'dart:async';
import 'package:fake_async/fake_async.dart';
import 'package:test/test.dart';
void main() {
test("Future.timeout() throws an error once the timeout is up", () {
// Any code run within [fakeAsync] is run within the context of the
// [FakeAsync] object passed to the callback.
fakeAsync((async) {
// All asynchronous features that rely on timing are automatically
// controlled by [fakeAsync].
expect(Completer().future.timeout(Duration(seconds: 5)),
throwsA(isA<TimeoutException>()));
// This will cause the timeout above to fire immediately, without waiting
// 5 seconds of real time.
async.elapse(Duration(seconds: 5));
});
});
}

Is return value important in Firebase Cloud Functions

I am writing the Firebase Could Functions with TypeScript and the following is a simple method to update a document.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(functions.config().firebase);
export const handleTestData = functions.firestore.document('test/{docID}').onCreate(async (snap, context) => {
const data = snap.data();
if (data) {
try {
await admin.firestore().doc('test1/' + context.params.docID + '/').update({duplicate : true});
} catch (error) {}
}
});
In this method, the promise is handled by async await and there is no return statement and it's working fine. Most of the examples/tutorials I have seen always have a return statement in each method.
Is there any impact/difference I don't return anything in Firebase Cloud Functions? If I should return something, can I return null?
Is return value important in Firebase Cloud Functions?
Yes, it is really key, in a Cloud Function which performs asynchronous processing (also known as "background functions") to return a JavaScript promise when all the asynchronous processing is complete, as explained in the documentation.
Doing so is important for two main reasons (excerpts from the doc):
You make sure that the Cloud Functions instance running your Cloud Function does not shut down before your function successfully reaches its terminating condition or state.
You can avoid excessive charges from Cloud Functions that run for too long or loop infinitely.
Why is your Cloud Function running correctly even if you don't return a Promise?
Normally your Cloud Function should be terminated before the asynchronous operations are completed, because you don't return a Promise and therefore indicate to the Cloud Functions platform that it can terminate the Cloud Functions instance running the Cloud Function.
But sometimes, the Cloud Functions platform does not terminate the Function immediately and the asynchronous operations can be completed. This is not at all guaranteed and totally out of your control.
Experience has shown that for short asynchronous operations this last case happens quite often and the developer thinks that everything is ok. But, all of sudden, one day, the Cloud Function does not work... and sometimes it does work: The developer is facing an "erratic" behaviour without any clear logic, making things very difficult to debug. You will find a lot of questions in Stack Overflow that illustrate this situation.
So concretely, in your case you can adapt your code like:
export const handleTestData = functions.firestore.document('test/{docID}').onCreate(async (snap, context) => {
const data = snap.data();
if (data) {
try {
// See the return below: we return the Promise returned by update()
return admin.firestore().doc('test1/' + context.params.docID + '/').update({duplicate : true});
} catch (error) {
return null; // <- See the return
}
} else {
return null; // <- See the return
}
});
or like
export const handleTestData = functions.firestore.document('test/{docID}').onCreate(async (snap, context) => {
const data = snap.data();
if (data) {
try {
await admin.firestore().doc('test1/' + context.params.docID + '/').update({duplicate : true});
return null; // <- See the return
} catch (error) {
return null; // <- See the return
}
} else {
return null; // <- See the return
}
});
Returning null (or true, or 1...) is valid since an async function always returns a Promise.

Cloud Functions to Cloud FireStore running locally but not when deployed

I try to write a document to one of the subcollections in firestore. The code when served locally writes to firestore but when I deploy it, it doesn't write anything.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
var db = admin.firestore();
exports.update = functions.https.onRequest((request, response) => {
db.collection('emails').doc(request.query.trackingid).get()
.then( doc => {
if (!doc.exists) {
console.log('No such document!');
} else {
var viewRef = db.collection('emails').doc(request.query.trackingid).collection('views');
var view = {
when: (new Date()).toUTCString()
};
viewRef.add(view)
.then(ref => {
console.log("Document added");
return;
}).catch(err => {
console.log("Document creation failed", err);
});
}
return;
}).catch((err) => {
console.log('Tracking ID not found', err);
return;
});
response.sendStatus(200);
});
You're sending a response before the work can complete. For HTTP type functions, you are obliged to send a response only after all the work is complete. Cloud Functions will forcible terminate the function after the response is sent.
Note that get() and all of the promises derived from it are asynchronous, meaning that they return immediately, with the callbacks only being called when the work is complete. And you have no guarantee about when that will be.
What your code is doing now is kicking off a get(), then immediately following up with the next line of code, which sends the response before the work is complete. When this response is sent, Cloud Functions terminates the function, and your async work may not complete.
You need to only send the response after you are sure everything is done. This involves understanding the structure of your promises in your code.
You may want to watch my video series on using promises in Cloud Functions to better understand how this works.

Firestore transaction fails with "Transaction failed all retries"

I'm running a very simple Firestore transaction which checks for the presence of a document, before writing to it if absent.
(The use case is registering a username - if it's not already registered, the current user gets to grab it)
Here's a snippet of the relevant Flutter code:
DocumentReference usernameDocRef =
Firestore.instance.collection(_USERNAMES).document(username);
await Firestore.instance.runTransaction((transaction) async {
var snapshot = await transaction.get(usernameDocRef);
if (!snapshot.exists) {
transaction.set(usernameDocRef, {
_UsernamesKey.userid: _user.id,
});
}
});
This is failing with an exception "Transaction failed all retries".
Based on the Firestore documentation, failure can occur for two reasons:
The transaction contains read operations after write operations. Read operations must always come before any write operations.
The transaction read a document that was modified outside of the transaction. In this case, the transaction automatically runs again. The transaction is retried a finite number of times.
I don't think I trigger either of those. Any suggestions?
The example transaction in the documentation uses await on its call to update. Perhaps you need the same on your call to set:
await Firestore.instance.runTransaction((transaction) async {
var snapshot = await transaction.get(usernameDocRef);
if (!snapshot.exists) {
await transaction.set(usernameDocRef, {
_UsernamesKey.userid: _user.id,
});
}
});
Firstly, try using the reference from the fresh snapshot and not from the original document reference. If this doesn't work, try changing [set] to [update] as I remember having the same error as you have experience now.
DocumentReference usernameDocRef =
Firestore.instance.collection(_USERNAMES).document(username);
await Firestore.instance.runTransaction((transaction) async {
var snapshot = await transaction.get(usernameDocRef);
if (!snapshot.exists) {
await transaction.update(snapshot.reference, {
_UsernamesKey.userid: _user.id,
});
}
});
This has recently been fixed - https://github.com/flutter/plugins/pull/1206.
If you are using the master channel, the fix should be available already. For the other channels (dev, beta, stable) YMMV.
I'm not a Flutter/Dart expert, but I expect you have to return something from within the transaction, so that Firestore knows when you're done:
await Firestore.instance.runTransaction((transaction) async {
var snapshot = await transaction.get(usernameDocRef);
if (!snapshot.exists) {
return transaction.set(usernameDocRef, {
_UsernamesKey.userid: _user.id,
});
}
})

redux asynchronous action with await/async

From the tutorial located here, I have a question regarding this section of the code:
export function fetchPosts(subreddit) {
// Thunk middleware knows how to handle functions.
// It passes the dispatch method as an argument to the function,
// thus making it able to dispatch actions itself.
return function (dispatch) {
// First dispatch: the app state is updated to inform
// that the API call is starting.
dispatch(requestPosts(subreddit))
// The function called by the thunk middleware can return a value,
// that is passed on as the return value of the dispatch method.
// In this case, we return a promise to wait for.
// This is not required by thunk middleware, but it is convenient for us.
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
// Do not use catch, because that will also catch
// any errors in the dispatch and resulting render,
// causing an loop of 'Unexpected batch number' errors.
// https://github.com/facebook/react/issues/6895
error => console.log('An error occured.', error)
)
.then(json =>
// We can dispatch many times!
// Here, we update the app state with the results of the API call.
dispatch(receivePosts(subreddit, json))
)
}
}
Let's assume I wanted to use the async/await syntax instead of the "then" syntax, how would I get the error object if something fails?
e.g.
let response = await fetch(`https://www.reddit.com/r/${subreddit}.json`)
let json = await response.json();
I can surround these lines of code with a try/catch, but the author has a stern warning not to use catch here (refer to snippet above).
So is there a proper way to use the async/await pattern with this code?
In the link you provided the note to avoid using catch is regarding the promise .catch statement. This is because it would catch errors in both the then blocks. Instead of just errors caused via fetch or response.json() it would also catch errors caused via dispatch(receivePosts(subreddit, json))
You should be able to use async await as you describe in your post whilst avoiding catching errors caused by dispatch. e.g.
export function fetchPosts(subreddit) {
return async function (dispatch) {
dispatch(requestPosts(subreddit));
let response;
let json;
try {
response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
json = await response.json();
} catch(e) {
// handle fetch or json error here e.g.
dispatch(receivePostsError(subreddit, e.message));
}
if (json) {
dispatch(receivePosts(subreddit, json));
}
}
}

Resources