POC Row event sequence stamping for submissions#1699
Conversation
d9952fa to
f673225
Compare
f673225 to
93c842b
Compare
| CREATE TABLE current_event ( | ||
| event bigint NOT NULL | ||
| ) |
There was a problem hiding this comment.
When we will add this mechanism for other tables then what will be the value of this? I am assuming it will be just existing value + number of rows in that table
There was a problem hiding this comment.
Up to us. We can choose to pretend that all rows in that other table were added at the same time (which is not what I did in the migration), in which case it'd be existing value + 1, or we can indeed do what you suggest.
Another thing we could do is to use 0 for everything that needs to be retroactively eventstamped. But then we'd impact the ability to use it as a cursor - the first chunk of a collection, for retro-applied eventstamps (existing deployments), for event 0, will be potentially huge. On the plus side we'd gain some measure of causal consistency, eg a submission won't exist "before" the project it belongs to. But that causal ordering can break anyway since only few of our tables are append-only (a project can be modified, in which case its eventstamp is going to be higher than that of any of the submissions that have been made for it, until those get modified).
93c842b to
1629c8e
Compare
8e0eb2a to
4eb43e3
Compare
4eb43e3 to
0186bf1
Compare
Related: getodk/central#1439 . With the note that here we are not necessarily going for fast revalidation, explained below.
This is the DB part of per-row-eventstamps for submissions. We can do other tables as well but since this is a POC, I'm leaving it at this for now.
I've chosen to try to do it with an application-transparent approach. The appeal is that it "just" works, as evident from the absence of any application modifications.
But to make that bliss possible, I went down the path of using a deferred constraint trigger to apply the stamps automatically at commit time, and there's some trickyness around that which led to some unexpected code. It probably needs an in-person walkthrough to answer all the "why not just..." type questions that would also jump to my mind if I hadn't just tried to "just ..." and found out why one can't "just ...".
Approach
max(event)(+row count, to account for deletions) on a projection to work as an etag (or cursor), we need strict ordering, so we need to prevent later-committed-rows having an earlier-generated (or, well, "sorted-earlier") eventstamp. Can't use a sequence. Can't use timestamps.xid8transaction IDs — two reasons: main one being that the semantics of those are that they can wrap around, vacuuming can do that. Transaction IDs are only required to be unique among all currently ongoing transactions, not among all transactions that ever happened. So we'd get duplication pretty quickly I fear. Second (and minor) reason is that they're not pretty.MIN_BIGINT(some negative number) so that we can do twice the number of (committed, observable) events before running into trouble" — Yeah we could but we'd forever be staring down very large negative numbers, so, trumped by aesthetics/DX I suppose?NEW. But we can make it have side effects (booooo!), thus writing a table anyway, which is how the event stamps are actually set. It's a bit of a perversion but I haven't found any alternatives. This, again, forces our hand:blank_submissions_event_*) that superficially may seem like excessive machinery come from.Performance impact
A quick & dirty peek (with 10000 submission POSTs, with the worker system turned off):
Some impact is to be expected, there's no free lunch, we're doing more stuff. I'm not panicking.
I've clocked the overhead at 89µs per row by just timing updating 10_000 submission rows with a new timestamp, comparing master with this branch. But anyway we don't usually do such large updates.
What has been done to verify that this works as intended?
CI
Why is this the best possible solution? Were any other approaches considered?
There are many ways to stroke a cat. When it comes to building blocks for etags, another approach was (is?) in #1654. For the etag use case this approach here has a different granularity tradeoff, namely, it chooses maximum granularity (at the row level), whereas #1654 was granular to the actee level. Consequently with the approach here validating the etag of a projection requires reprojection (rerunning the query with its filters, and calculating the max(event) of the submissions selected), whereas in #1654 the etag is validated through a small index (and doesn't even hit the heap!).
Besides granularity for etags, there is the topic of suitability as a cursor. The hope is that we can use this safely as a cursor. IIUC we can (safely, under concurrency) but I'd feel better if some others come to to that same conclusion independently.
How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?
Does this change require updates to the API documentation? If so, please update docs/api.yaml as part of this PR.
Before submitting this PR, please make sure you have:
make testand confirmed all checks still pass OR confirm CircleCI build passes