38

Firebase allows for a resource to be updated transactionally. As I understand it, the client does this buy sending requests to the server saying "If the old value is X, make the new value Y". If there is contention the server could reject multiple updates form the client until one is accepted.

Now, what if I want to update multiple resources atomically?

What happens if the first update is accepted, and then the client is disconnected prior to the second update being accepted. Is there any way to enclose multiple updates in an atomic transaction? If not, is there an idiomatic solution to this problem?

WW.
  • 23,793
  • 13
  • 94
  • 121

2 Answers2

44

UPDATE

It's now possible to update multiple locations atomically. See this blog post for details.

var mergedUpdate = {};
mergedUpdate[ 'users/' + userId + '/widgets/' + widgetId ] = true;
mergedUpdate[ 'widgets/' + widgetId ] = widgetData;

var ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/");
ref.update(mergedUpdate);

This doesn't enforce transactional data (if value is currently X, make it Y), but this part can be moved to security rules. For example, if we want to update two counters at the same time, we could add rules as follows:

{
  "counter1": {
     ".validate": "newData.val() === (data.val()||0)+1"
  },

  "counter2"1 {
     ".validate": "newData.val() === (data.val()||0)+1"
  }
}

Now we can attempt the same multi-path update as above. If the values have changed since we last read them from the server, the attempt will fail. We can check if( error.code === 'PERMISSION_DENIED' ) { ... } to see if the failure was due to validation, and retry accordingly.

ORIGINAL POST

The only way to do this is to run a transaction on a common ancestor.

For instance, if you want to update /a/b/c and /a/x/y, you could run a transaction at /a and change both of the values.

The downside to this approach is that it can be expensive with network I/O, as all of the data inside the transaction needs to be downloaded and then sent back to the server.

A more complicated but potentially more powerful approach you might want to consider is restructuring your data so that rather than storing the actual values, you store a history of the edits. For example, if you're storing bank balance information, you could store a history of deposits and withdrawals. Then when you wanted to get the balance, you would play back the whole history and calculate the end balance.

The beauty of this approach is it lets you do atomic updates. For example, if you were transferring money from accountA to accountB, you'd just append an element to the end of the log saying "transfer from account A to accountB N dollars". Appending that single element is an atomic operation.

This is the approach we take with Firepad, our collaborative text editor.

Kato
  • 40,352
  • 6
  • 119
  • 149
Andrew Lee
  • 10,127
  • 3
  • 46
  • 40
  • Thanks for your answer. I conceptually like storing new transactions rather than updating in place, but I think this is only workable from a performance perspective if there is a query materialization feature. Replaying all of history would be slow. – WW. Jul 03 '13 at 01:45
  • There isn't one (at least not yet), but we definitely see how this would be useful! For now you can materialize the query on the client and store it back to a "checkpoint" in Firebase (this is how Firepad works), or you could have a node.js or Java-based server somewhere running as a Firebase client that materializes it for you. – Andrew Lee Jul 03 '13 at 15:31
  • HI! Thanx, but I just tried that and got an error: "Line 10: Left operand of || must be boolean." Am I missing something? I'm using Firebase 3.0... – Matheus Dal'Pizzol May 24 '16 at 16:41
  • 4
    While this answer is correct for multi-location updates, I would strongly suggest an edit that indicates this counter example is not safe from malicious incrementing. In other words, just because you _can_ write to the two endpoints simultaneously doesn't mean you _have_ to. The Firebase validation you supply doesn't prevent a malicious user from continuing incrementing the counter at `'widgets/' + widgetId` without adding any new data to the `'users/' + userId + '/widgets/' + widgetId` location unless I am mistaken. Thanks! – Matt May 29 '16 at 00:34
  • For the record, that blog post referenced at the top of this answer appears to have the wrong examples in it, at least as of this comment (the only comment on that blog post makes mention of the same thing). – hairbo Jun 03 '16 at 22:15
0

there is another way.... very tedious... and not really transactions...

As Firebase allows atomically changes on a value, you could create a lock or semaphore with the meaning of "started a transaction on the resources x, y and z"... of course, it is not a real transaction as it doesn't block resources, a part from the lock. We could call it transaction by convention. The clients needs to know that when the lock is occupied, they should not change x, y and z resources....

Seb
  • 500
  • 5
  • 13
  • 2
    this breaks when two clients simultaneously ask for the semaphore. not a real lock so it doesnt solve the concurrency issue, just reduces chances. – Zig Mandel Dec 23 '15 at 17:02