10

I'm trying to understand the way the conflict resolution works in firebase for which I need some help.

Assuming that I've json object saved in a node in firebase realtime:

{
    "shape": "rectangle",
    "stroke": 10, 
    "color": "black"
}

I've defined a test page which reads this data and displays and also listens to the changes happening on node with key in realtime. I've added a provision to update the data which eventually updates the specific key value alone.

Sample used case

client 1 - loads the page
    data - {"shape": "rectangle", "stroke": 10, "color": "black"}
client 2 - loads the page 
    data - {"shape": "rectangle", "stroke": 10, "color": "black"}

client 2 goes offline
client 2 updates stroke value to 20 
    data - {"shape": "rectangle", "stroke": 20, "color": "black"}
* data is yet to sync to the server

client 1 makes a change after client 2 has already done with its changes and changes stroke to 5
    data - {"shape": "rectangle", "stroke": 5, "color": "black"}
* data gets synced to the server immediately


client 2 comes online and pushes its changes and overrides the changes made by client 1
    data - {"shape": "rectangle", "stroke": 20, "color": "black"}

Ideally since the client 1 made the change at a later point of time than client 2 the client 1 changes should be retained when client 2 data gets synced.

I would be very glad if somebody can suggest me a way to such type of conflict resolution in firebase(may be by defining some rules and some extra logic).

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Naveen
  • 121
  • 2
  • 7
  • I was going to recommend you to add a timestamp to your JSON object and use security rules to ensure that the data being written is the most recent one. But then I saw on the [Firebase Reference](https://firebase.google.com/docs/reference/js/firebase.database.ServerValue#.TIMESTAMP) that the `ServerValue.TIMESTAMP` contains "the current timestamp as determined by the Firebase servers". Now I'm wondering what time will be shown when the data that client2 pushed to the database gets written. I'll wait for someone else to answer. – Rosário Pereira Fernandes Feb 16 '18 at 08:22

1 Answers1

15

With your current code, the expected behavior is indeed that the last write wins.

There are two other options:

  1. Use a transaction to detect that the data was changed already, and then retry.
  2. Prevent conflicts altogether, by using a data structure that separates the writes from each client.

Let's have a look at each in turn.

Using transactions is the most common way around this problem. When you use a transaction in Firebase, the client sends a "compare and set" operation to the server. This is an instruction of the type: "if the current value is A, set it to B". In your scenario that means that the second write detects that the stroke has already changed, so it gets a retry.

To learn more about transactions have a look at the Firebase documentation, and at my answer here about how they work.

This may sound like a great solution, but it does unfortunately affect the scalability of your code. The more users are trying to modify the same data, the more likely the transaction has to retry. That's why it's always good to consider if the conflict can be avoided altogether.

Preventing conflicts is the best conflict resolution strategy out there. By preventing conflicts, you never have to resolve them, which means you never have to write code to resolve conflicts, which means that your application will scale a lot better/farther.

To prevent conflicts, you'll want to look for a data structure where your users are always writing to unique locations. In your use case, instead of having each client update the stroke, you could instead have the client write their "update action" to a queue of updates. For example:

shapes
  shapeid1
    pushid1: {"shape": "rectangle", "stroke": 10, "color": "black"} /* initial data */
    pushid2: { "stroke": 5 } /* first update */
    pushid3: { "stroke": 20 } /* second update */

In this data structure, nobody is overwriting anyone else's data (something that is easy to enforce in security rules). Everyone is just appending new updates to the shape (using ref.push(), which generates unique locations in chronological order).

To get the current data for a shape, each client will need to read all updates for that shape and recalculate them on the client. For most use-cases I've seen that's a simple operation, but if not: it is quite easy to have a Cloud Function calculate a periodic snapshot of the state.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Preventing conflicts approach sounds elegant. However i doubt if the transactions will help since the client can make the changes when offline(not connected to server) and the client may or may not have the latest. Hence i was wondering if there is someway to alter the way the sync happens using some timestamps so that the latest update wins. – Naveen Feb 16 '18 at 09:35
  • Transactions indeed won't work when you're offline. That's another reason I prefer preventing conflicts: `push()` generates keys that stay ordered, even when you're offline. But there's always a chance of a "huh?" moment after a user comes back online. – Frank van Puffelen Feb 16 '18 at 10:07
  • 3
    One thing you can do is have a client-side timestamp in each update. Your security rules can then reject updates that it considers out of date by comparing the timestamp with `now`. – Frank van Puffelen Feb 16 '18 at 10:40
  • even with a defined timestamp, i believe defining the rules will get more trickier and elaborate when the user updates different fields when offline/online. For instance if offline client (c2) updates color and stroke and the online client (c1) updates just the stroke, then post conflict resolution i would expect the result to have the color as defined by c2 and stroke as defined by c1. Such used cases makes me think that transactions would not help to handle such scenarios. I hope you agree with that – Naveen Feb 16 '18 at 11:18
  • It all depends on how you define an "update" and how you define a "conflict". If you want to allow the use-case you described, you should store each individual property change as an "update". – Frank van Puffelen Feb 16 '18 at 11:32