erquhart
erquhart5mo ago

Is _creationTime guaranteed unique?

I've understood this to be true, but read in a post here (I can't find it now) something that hinted at this maybe not being a 100% guarantee. I have logic that relies on this value being unique, so just wanted to make sure.
4 Replies
lee
lee5mo ago
nope, not guaranteed. also not guaranteed to be monotonic, although both of these properties are usually true. by not monotonic, i mean it's possible to have: 1. cron job mutation reads the maximum _creationTime in table1 as 1000 and saves this in table2 as {cursor: 1000} 2. a user calls a mutation which inserts a document with _creationTime of 999 to table1. 3. the cron job runs again, reading the cursor from table2 and reads documents in table1 with _creationTime > 1000, and finds no documents. the cron job is trying to process all documents by cursoring on _creationTime, but it missed a document. you can avoid these race conditions by adding serialization to the table. when we originally shipped _creationTime we did have uniqueness and monotonicity guarantees, but it caused a lot of unnecessary contention because to guarantee it we needed to make mutations run in sequence; they couldn't run in parallel
erquhart
erquhartOP5mo ago
Super helpful, thank you. Can you say a bit more (or point to something that says more) on adding serialization to a table? Or are you just talking about not doing parallel writes in my mutations Even if I did that, there's no guarantee that two different users won't have mutations commit at the same time to a single table. For example, I have plaid updates coming in via webhook, collisions between these and not having the ordered/unique guarantees on creationTime are my current concern.
lee
lee5mo ago
for sure. the typical way of serializing writes to a table is having an incrementing field. so something like
const maxCounter = (await ctx.db.query("tbl").withIndex("counter").order("desc").first())?.counter ?? 0;
await ctx.db.insert("tbl", { ...fields, counter: maxCounter + 1 });
const maxCounter = (await ctx.db.query("tbl").withIndex("counter").order("desc").first())?.counter ?? 0;
await ctx.db.insert("tbl", { ...fields, counter: maxCounter + 1 });
this mutation takes a read dependency on the [maxCounter, infinity) range of the tbl.counter index, and then inserts at maxCounter + 1. Two mutations that insert into "tbl" can't read the same value for maxCounter and then both commit, because that wouldn't be serializable. So now you have maxCounter as a unique monotonic value that you can use for cursoring or whatever. But it also means mutations can't insert concurrently into "tbl". if you just want uniqueness, not monotonicity, I would use the _id field (although you can do something similar with a counter that checks that the exact counter value doesn't exist before inserting)
erquhart
erquhartOP5mo ago
Okay, that makes sense, thanks for the explanation here.