5

I am using transactions to write to a specific location in firebase realtime database with firebase admin api in my nodejs app. I observed that the transaction handler gets called twice, even when there are no other clients using the database.

Following is a minimal code which displays this behavior.

firebaseAdmin.database().ref('some/path').transaction(currentData => {
    console.log('transaction handler got called');
    return {'abc': 'def'};
}, null, false).then(value => {
    console.log('transaction complete')
}).catch(reason => {
    console.log('transaction failed. ' + reason);
});

I can observe that transaction handler got called gets logged twice for each execution of above code.

I understand that the handler can get called multiple times if some other client writes to the db path in the window between currentData is read for a transaction and the new data is attempted to be committed to the db path. But, there are no other clients in my case, so I cannot understand why the transaction handler needs to get called twice.

Does anyone know what is the reason for this?

Lahiru Chandima
  • 22,324
  • 22
  • 103
  • 179

1 Answers1

6

This is expected behavior. When you run a transaction, the Firebase client immediately calls your transaction handler with its best guess of the current value of some/path. The first time you run it, this best guess is typically null. If some/path already exists that is always wrong, and will always lead to a second call to your transaction handler once the client has the correct current value.

In a flow chart it looks something like this

 app code   client                   server
              +                         +
transaction() |                         |
              |+--+                     |
              |   |current == null      |
              |   v                     |
              |   |new = 0              |
              |<--+                     |
              |                         |
              |  current==null, new=0   |
              |+----------------------->|
              |                         |+--+
              |                         |   |current != null
              |                         |   v
              |                         |   |current = 0
              |                         |<--+
              |    NACK, current=0      |
              |<-----------------------+|
              |                         |
              |+--+                     |
              |   |curent==0            |
              |   v                     |
              |   |new=1                |
              |<--+                     |
              |                         |
              |  current==0, new=1      |
              |+----------------------->|
              |                         |+--+
              |                         |   |current == 0
              |                         |   v
              |                         |   |current = 1
              |                         |<--+
              |    ACK, current=1       |
              |<-----------------------+|
              |                         |
              +                         +

Also see these explanations of how transactions work:

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • So this done as an optimization to parallelly execute the transaction handler while sdk is fetching current data from the server? – Lahiru Chandima Jul 22 '19 at 00:52
  • There is no parallelism here. But if the client doesn't know a current value, it needs to get that from the server. It might as well try with `null` on that first try. – Frank van Puffelen Jul 22 '19 at 01:53
  • 2
    Then why doesn't the sdk just call the handler only after it knows the correct value from the server? I don't see any point on calling the handler without knowing the correct value, since once the correct value is fetched from the server, it will anyway have to call the handler again. – Lahiru Chandima Jul 22 '19 at 02:43
  • 1
    Imagine that the node doesn't exist. Now the initial value your handler returns is correct, and we've saved a roundtrip. There are of course other default values, or other algorithms, that could have been used, but this is the one that the Realtime Database uses. – Frank van Puffelen Jul 22 '19 at 03:55