Is it possible to observe partial changes from an atomic Firestore write?

286 views
Skip to first unread message

apa...@gmail.com

unread,
Jun 12, 2018, 9:02:55 PM6/12/18
to Firebase Google Group

Hi.


(This is my first post here - I hope it's in line with what you except)


The Firestore docs says that both transactions and batched writes are atomic operations - either all changes are written or nothing is changed.


This question is about whether the changes of an atomic operation in Firestore can be partially observed, or whether the all or nothing guarantee applies to readers too?


Example:


Let's say that we have a Firestore database with at least two documents, X and Y.


Let's also say that there are at least two clients (A and B) connected to this database.


At some point client A executes a batched write that updates both document X and Y.


Later, client B reads document X and observes the change that client A made.


Now, if client B would read document Y too, is there a guarantee that the change made by A (in the same batched write operation) will be observed?


(Assuming that no other changes where made to those documents)


I've tested it and I've never detected any inconsistencies. However, just testing this matter can't be enough. It comes down to the level of consistency provided by Firestore, under all circumstances (high write frequency, large data sets, failover etc)


It might be the case that Firestore is allowed (for a limited amount of time) to expose the change of document X to client B but still not expose the change of document Y. Both changes will eventually be exposed. 


Question is; will they be exposed as an atomic operation, or is this atomicity only provided for the write?


Thank you for any insights

/ Mårten

Gil Gilbert

unread,
Jun 15, 2018, 3:33:38 PM6/15/18
to Firebase Google Group
The Firestore Server SDKs are very easy to reason about, since they directly query the backend. If B has seen A's changes to X then when B reads Y it will see A's changes there too. The only way to cause this to not be true is to intentionally use a separate transaction created before A's writes for the second read.

The Mobile and Web SDKs are more complicated. At a high level you can think of these SDKs as moving monotonically from one global snapshot to the next global snapshot. At this high level view A's changes are applied within a single snapshot. Where things get complicated is the interaction with the offline cache.

In more detail Firestore makes the following guarantees:
  • Writes are atomic (as documented; I won't discuss this here)
  • Single queries are internally consistent
  • If persistence is enabled, you can see inconsistent results but you can opt into consistent results (at the expense of latency)
  • With persistence disabled, monotonicity is only guaranteed within a session
Single queries with persistence disabled are straightforward. In your examples, this means that if B executes a single query whose results contain both X and Y, you're guaranteed to see them either before A's change or after but not a mix.

With persistence enabled, it's possible for stale results in the cache to make things appear inconsistent. Consider the following timeline:
  1. B queries for X and Y, receives both at version 1.
  2. A updates X and Y to version 2.
  3. B listens for X
    1. First snapshot is from cache: X1
    2. Second snapshot is from the server: X2
  4. B listens for Y
    1. First snapshot is from cache: Y1
    2. Second snapshot is from the server: Y2
At step 4.1 you can consider yourself to have seen an inconsistent result. If this case is important, the solution is to include metadata updates in your snapshot listener (i.e. with query.onSnapshot({ includeMetadataChanges: true }, observer) with the Web SDK) and then wait to raise events until you see a snapshot with metadata.fromCache == false. Waiting for metadata.fromCache == false is not our default behavior for listens because it adds considerable latency.

This kind of scenario can also play out even within a single query if, for example, B stopped after 3.2 above and then started a single query for both X and Y it would see a first snapshot from cache containing X2 and Y1.

Note that for gets our default behavior is to attempt to wait for an update from a server but fall back on the cache if the server is unable to provide results in a timely manner. We've recently added "source" controls which allow you specify the behavior you want.

With persistence disabled an alternative scenario is possible:
  1. A performs its write
  2. B listens for X, sees V2
  3. B restarts
  4. B listens for Y, sees V1
This is possible because the server is only required to advance snapshots monotonically for a given client session. When B restarts it's effectively a new client and could hit a new server that happens to restart at a snapshot earlier than A's writes. If you enable persistence, the client keeps track of the highest snapshot version it has seen and discards any snapshots older than that for you, preventing this scenario.

So if you have persistence enabled, and wait for snapshots with metadata.fromCache == false your reads will always be consistent. The cost is significantly increased latency since every new query must wait for the server. Most applications prefer possibly stale results immediately over guaranteed consistency.

I hope this helps.

Cheers,
-Gil

Mårten Wikström

unread,
Jun 15, 2018, 5:41:00 PM6/15/18
to fireba...@googlegroups.com
Thank you Gil for your excellent response!

I'm very happy to learn that Firestore provide this "global snapshot" behavior.

The case I'm working on is one where I have a Cloud Function (using the Admin SDK) that aggregate data from multiple collections into an XML document. This function is invoked frequently (~1.5M times per day) and yields quite a few read operations. Even with CDN caching and ETag handling this incur a substantial cost.

I've therefore introduced a "cache token" document which is changed by Cloud Function Triggers whenever a change is made to one of the "source collections". The Cloud Function that generate the aggregated data can then check this cache token in a single read operation and see if it matches the token for its cached aggregated data.

This caching feature has pulled down costs considerably but I was worried that maybe the aggregating Cloud Function would see a new "cache token" but still not see the relevant "source collection" changes. I've therefore added a "volatile time period" after a new cache token is detected. During this period the aggregating Cloud Function is not allowed to use its cache.

I had no idea what would be a good length for this time period and knew for sure that I could never guarantee consistency with that approach. With the information you have provided, it is now clear that the "volatile time period" is zero. Consistency is guaranteed! Happy day!

Again, thank you so much for taking the time to explain this behavior and doing so in a detailed yet easy to understand way.

/ Mårten

--
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/89XxRTKpYwQ/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-tal...@googlegroups.com.
To post to this group, send email to fireba...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/firebase-talk/7ee18c16-69b2-4b58-966d-e08aa1d0c379%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Reply all
Reply to author
Forward
0 new messages