title: Firebase realtime database transaction and cloud functions
body: <p>I have a list in the Firebase realtime database, structured as follows:</p>
  
  <pre><code>list: {
    change: {
      rate: integer,
      last: Date
    },
    lock: string,
    elements: {
      ...
    },
    size: integer
  }
  </code></pre>
  
  <p>Clients only add elements to the list.
  There is a cloud function triggered by <code>onCreate</code> event on elements' child. It transactionaly increments <code>list.size</code> in the database and then performs calculations on elements possibly involving removing all or some children.
  And there is another cloud function triggered by <code>onDelete</code> event on elements' children which only transactionaly decrements <code>list.size</code> in the database.
  The issue is that the counter becomes inaccurate after some time. Sometimes it shows more elements that there are in the list, sometimes less.
  Below is the [adapted] code:</p>
  
  <pre class="lang-js prettyprint-override"><code>exports.element_add = functions.database.ref('/list/elements/{element}).onCreate(
    (snapshot, context) =&gt; {
      const elements = snapshot.ref.parent;
  
      return new Promise(
        (resolve, reject) =&gt; {
          const start = new Date().getTime();
  
          async.waterfall(
            [
              // read and write to list.change
              // ...
              (next) =&gt; {
                // increment list size
                _changeSize(elements.parent, +1, 1, next)
              },
              // acquire lock if required and do calculations on list elements
              // ...
            ],
            // finally
            function (e) {
              // release the lock
              // ...
              // handle errors
              if (e)
                return reject(e);
  
              resolve();
            }
          );
        });
    });
  
  
  exports.element_remove = functions.database.ref('/list/elements/{element}).onDelete(
    (snapshot, context) =&gt; {
      const elements = snapshot.ref.parent;
  
      return new Promise(
        function (resolve, reject) {
          _changeSize(elements.parent, -1, 1,
            function (e) {
              if (e)
                return reject(e);
  
              resolve();
            }
          )
        }
      )
    });
  
  function _changeSize(list, delta, attempt, done) {
    list.child('size').transaction(
      // update
      (size) =&gt; {
        if (size === null)
          return null;
  
        return size + delta;
      },
      // finally
      (e, committed, snapshot) {
        if (e) {
          console.error(`size transaction error: ${e}, retrying in 1 sec`);
  
          // try again
          return setTimeout(_changeSize, 1000, list, delta, attempt + 1, done);
        }
  
        console.log(committed ? `new list size is ${snapshot.val()}` : 'list size was not updated');
  
        // return new size if committed
        done(null, committed ? snapshot.val() : undefined);
      },
      // do not apply locally
      false
    );
  }
  
  </code></pre>
  
  <p>Except for the two cloud functions above no other entity is working with <code>list.size</code>. The question is why is the value of <code>list.size</code> gets out of sync with <code>list.elements</code>?</p>
  
  <p><strong>Update 2020-02-26</strong>: Apparently some of onCreate/onDelete events is being received more than once! We've observed them arriving even 3 times spread between instances of the cloud function. Is it a normal behavior or something we're doing wrong?</p>