Jason Berryman
Jason Berryman

Reputation: 4898

How to retry a Cloud Firestore transaction in a Cloud Function until state == true

The issue

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?

  1. Document.state is set to stage1
  2. Document.state is updated to stage2
  3. Document.state is updated to stage3
  4. Cloud Function is triggered and reads stage3
  5. Cloud Function is triggered and reads 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)

Sample code

Variables passed in

myDocumentRef: db.doc('myCollection/myDocument')
newState: stage3

Transaction code

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

Answers (2)

Falco
Falco

Reputation: 3416

There are basically two options for your problem:

1. Use the Google Cloud RETRY flag

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
    }
})

2. Create your own retry logic

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

Doug Stevenson
Doug Stevenson

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

Related Questions