Reputation: 4898
I have a Cloud Function which updates a Cloud Firestore document, based on changes in a document state
field. These updates must happen in a particular order. When two changes are made in quick succession, there's no guarantee that the Cloud Functions will run in the correct sequence. Is there a way to have a Cloud Firestore transaction retry until it succeeds or times out?
Document.state
is set to stage1
Document.state
is updated to stage2
Document.state
is updated to stage3
stage3
stage2
In the Cloud Functions documentation, it discusses the ability to retry transactions on failure. However, this option is greyed out in the Cloud Functions section of the GCP Console (not shown in the Firebase Console)
myDocumentRef: db.doc('myCollection/myDocument')
newState: stage3
var transaction = db.runTransaction(t => {
return t.get(myDocumentRef)
.then(doc => {
if ((newState = 'stage2' && doc.data().state = 'stage1') ||
(newState = 'stage3' && doc.data().state = 'stage2')) {
t.update(myDocumentRef, { population: newPopulation });
} else {
// Keep retrying the transaction until it succeeds
}
});
}).then(result => {
console.log('Transaction success!');
}).catch(err => {
console.log('Transaction failure:', err);
});
Upvotes: 2
Views: 5329
Reputation: 3416
There are basically two options for your problem:
When you deploy a function you can simply enable retries, which will lead to the google-cloud environment automatically invoking your function if it throws any error (or returns rejected Promise) This sounds quite good, until you realise this may create !significant charges!, because if you have a bug in your function, which throws an error every time, the function will be called thousands of times until the retry-limit is finally exceeded after a day or two.
Your function could look like this:
return t.get(myDocumentRef)
.then(doc => {
if ((newState = 'stage2' && doc.data().state = 'stage1') ||
(newState = 'stage3' && doc.data().state = 'stage2')) {
t.update(myDocumentRef, { population: newPopulation });
} else {
throw new Error('Illegal state')
}
});
}).then(result => {
console.log('Transaction success!');
}).catch(err => {
if (event.timeStamp < Date.now()-300000) {
// We have tried for 5 minutes and still an error, we give up
console.error('Bad things happen: ', err)
} else {
throw new Error(err) // Firebase will receive this as a rejected Promise and retry
}
})
If you just want a single retry after some time, you can simply return a Promise which will resolve via setTimeout() to wait a short amount of time and try again. This looks like a little more work, but you have a lot more control over the number of retries. But in contrast to the Firebase retries you will have to deal with the maximum runtime-limit of your function.
else {
return new Promise(r => window.setTimeout(tryUpdate, 100))
Upvotes: 2
Reputation: 317467
Firestore transactions retry themselves by default. The documentation for transactions states:
A transaction consists of any number of get() operations followed by any number of write operations such as set(), update(), or delete(). In the case of a concurrent edit, Cloud Firestore runs the entire transaction again. For example, if a transaction reads documents and another client modifies any of those documents, Cloud Firestore retries the transaction. This feature ensures that the transaction runs on up-to-date and consistent data.
This retry takes the form of repeated invocations of the transaction handler function (the function you pass to runTransaction).
The Cloud Functions retry mechanism is different. It retries functions that don't fully successfully. The details about how that works can be read here. It has nothing to do with Firestore transactions. The semantics of those retries are independent of the type of trigger used.
Upvotes: 5