Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In firebase, why don't transactions work in cloud functions like they do in the admin api?

I have existing admin api code that I've simplifile down to this for testing purposes (this works):

admin.database().ref('/dropbox').on('child_added', function (childSnap) {

  let item, itemRef = childSnap.ref;

  console.log(`Item: ${JSON.stringify(childSnap.val())} at ${childSnap.key}`);
  console.log(`Item ref: ${itemRef.toString()}`);

  itemRef.transaction(function (value) {
    console.log(`Value: ${JSON.stringify(value)}`);
    if (value) {
      item  = value;
      return null;
    }
  }).then(function (resolution) {
    console.log(`Txn resolution: ${resolution.committed ? 'committed' : 'NOT-COMMITTED'}`);
    if (resolution.committed) {
      // process item
      console.log(`Process: ${JSON.stringify(item)}`);
    } else {
      // assume that item must have been removed by someone else
    }
  }).catch(function (err) {
    console.log(`Txn error: ${JSON.stringify(err, null, 2)}`);
  });

});

When I run:

firebase database:push /dropbox <<<'{"test":"abc123"}'

The console output is:

Item: {"test":"abc123"} at -KgTpp3FzgbLUrMNofNZ
Item ref: https://cloud-function-txn-test.firebaseio.com/dropbox/-KgTpp3FzgbLUrMNofNZ
Value: {"test":"abc123"}
Txn resolution: committed
Process: {"test":"abc123"}

I've been trying to move my code and this example to a cloud function. I realize that .on('child_added', f) and .onWrite(f) treat existing data differently but I can't get the transaction code to work correctly. The parameter passed to my transaction function is always null.

As a cloud function (this does not work):

exports.receiveAndRemove = functions.database.ref('/dropbox/{entryId}').onWrite(function (event) {

  if (!event.data.exists()) {
    return;
  }

  let item, itemRef = event.data.adminRef;

  console.log(`Item: ${JSON.stringify(event.data.val())} at ${event.data.key}`);
  console.log(`Item ref: ${itemRef.toString()}`);

  itemRef.transaction(function (value) {
    console.log(`Value: ${JSON.stringify(value)}`);
    if (value) {
      item  = value;
      return null;
    }
  }).then(function (resolution) {
    console.log(`Txn resolution: ${resolution.committed ? 'committed' : 'NOT-COMMITTED'}`);
    if (resolution.committed) {
      // process item
      console.log(`Process: ${JSON.stringify(item)}`);
    } else {
      // bad to assume here that item must have been removed by someone else
    }
  }).catch(function (err) {
    console.log(`Txn error: ${JSON.stringify(err, null, 2)}`);
  });

});

For some reason, the transaction never removes the item. Log output:

2017-03-30T10:51:19.387565284Z D receiveAndRemove: Function execution started
2017-03-30T10:51:19.395Z I receiveAndRemove: Item: {"test":"abc123"} at -KgTpp3FzgbLUrMNofNZ
2017-03-30T10:51:19.395Z I receiveAndRemove: Item ref: https://cloud-function-txn-test.firebaseio.com/dropbox/-KgTpp3FzgbLUrMNofNZ
2017-03-30T10:51:19.396Z I receiveAndRemove: Value: null
2017-03-30T10:51:19.396Z I receiveAndRemove: Txn resolution: NOT-COMMITTED
2017-03-30T10:51:19.418446269Z D receiveAndRemove: Function execution took 32 ms, finished with status: 'ok'

Of course, the cloud function fails to remove the item and because the transaction didn't commit the remove, also doesn't process the item. I expect both to happen and I expect this code to work even when the node server version is running. The items should always be processed exactly once no matter how how many instances are running in the cloud and/or my server.

Is there some subtle difference in cloud functions I am missing? Is there something I'm doing with transactions incorrectly or that doesn't work with cloud functions?

Full source: https://github.com/mscalora/cloud-function-txn-test.git

like image 810
Mike Avatar asked Mar 30 '17 11:03

Mike


People also ask

What are the restrictions in Firebase?

Firebase Security Rules work by matching a pattern against database paths, and then applying custom conditions to allow access to data at those paths. All Rules across Firebase products have a path-matching component and a conditional statement allowing read or write access.

What type of Cloud Functions exist in Firebase?

Distinctly there are 2 types of Cloud functions: HTTP functions and Event-driven functions.

When should I use Firebase cloud function?

You should use Cloud Functions for Firebase if you're a developer building a mobile app or mobile web app. Firebase gives mobile developers access to a complete range of fully managed mobile-centric services including analytics, authentication and Realtime Database.


1 Answers

The problem here is that in the scenario when the transaction value is null, you return undefined, which cancels the transaction. You actually need to handle the case when the value is null since Firebase may pass that value. The reason for this dive a bit into how Firebase transactions work.

In the first example, you have a local listener on the node you are doing the transaction on. This means you have the exact value for that node stored in the local cache. In the second example, you have the value for the node, but there is no actual listener for that node locally. The value comes from Cloud Functions itself and is not stored locally. Thus, when you do the transaction, Firebase will try right away with a "guess" of the value, which is always null to start. The transaction will retry once it hears from the server that the value is not null, and the server will tell it what the new value is. Then, the transaction will retry. However, because you don't handle the null case and simply return undefined, the transaction is cancelled.

I don't think you really need a transaction for what you are trying to do though. You can get the value of item in both your code samples without doing a transaction. For example, here is how you can update your Cloud Functions example:

exports.receiveAndRemove = functions.database.ref('/dropbox/{entryId}').onWrite(function (event) {
  if (!event.data.exists()) {
    return;
  }

  let item = event.data.val();
  let itemRef = event.data.adminRef;

  console.log(`Item: ${JSON.stringify(item)} at ${event.data.key}`);
  console.log(`Item ref: ${itemRef.toString()}`);

  return itemRef.remove();
});
like image 169
jwngr Avatar answered Oct 15 '22 13:10

jwngr