Follow-up to #69 (fixed in #78), which made every transaction of one backend take its own db_version. Two backends writing at the same time can still end up with the same one.
Cause
Each backend picks its next db_version from its own cache, reloaded as the MAX(db_version) of the meta tables. Another backend's uncommitted rows are not visible to that query, so two transactions in flight both compute the same next value. #78 also makes seq restart at 0 in every transaction, so both can store the same (db_version, seq).
Reproduction
On main at 0c20064, one table, a second session through dblink:
INSERT INTO t VALUES ('base','x'); -- base@1/0
SELECT dblink_exec('a', 'BEGIN');
SELECT dblink_exec('a', 'INSERT INTO t VALUES (''a1'',''x'')'); -- in flight
INSERT INTO t VALUES ('b1','x'); -- commits first
-- served now: base@1/0 b1@2/0 -> since = 2
SELECT dblink_exec('a', 'COMMIT');
-- after a1 commits: base@1/0 a1@2/0 b1@2/0
-- db_version > 2: (empty)
a1 and b1 are separate transactions with the same (db_version, seq), 2/0.
- A download taken after
b1 committed ends at db_version 2. The next one asks for db_version > 2, so a1 is never delivered.
The same skip happens without a shared db_version whenever versions commit out of order. If A takes N and B takes N+1, and B commits and is served before A commits, A's changes land below a window that has already moved on.
Impact
Any server applying payloads or taking local writes on more than one connection at once, which includes a connection pool and concurrent apply jobs. Changes are lost for the peers downloading them: they are stored locally but never served. This predates #69; before #78, the ever-growing seq only hid the duplicate pairs, not the skip.
Directions
- Serialize allocation: take a transaction-scoped lock when a transaction first takes a db_version, held until commit. Writers then commit in version order, at the cost of serializing all writing transactions of the database.
- Serve only a safe horizon: keep allocation concurrent, but have the download windows (
cloudsync_payload_chunks, the db_version > since readers) stop below the lowest db_version still held by an in-flight transaction.
Either needs a test with two sessions, like the reproduction above, and a check of what the chunk tiling and the positional resume assume about commit order.
Follow-up to #69 (fixed in #78), which made every transaction of one backend take its own db_version. Two backends writing at the same time can still end up with the same one.
Cause
Each backend picks its next db_version from its own cache, reloaded as the
MAX(db_version)of the meta tables. Another backend's uncommitted rows are not visible to that query, so two transactions in flight both compute the same next value. #78 also makesseqrestart at 0 in every transaction, so both can store the same(db_version, seq).Reproduction
On
mainat0c20064, one table, a second session through dblink:a1andb1are separate transactions with the same(db_version, seq),2/0.b1committed ends at db_version 2. The next one asks fordb_version > 2, soa1is never delivered.The same skip happens without a shared db_version whenever versions commit out of order. If A takes N and B takes N+1, and B commits and is served before A commits, A's changes land below a window that has already moved on.
Impact
Any server applying payloads or taking local writes on more than one connection at once, which includes a connection pool and concurrent apply jobs. Changes are lost for the peers downloading them: they are stored locally but never served. This predates #69; before #78, the ever-growing
seqonly hid the duplicate pairs, not the skip.Directions
cloudsync_payload_chunks, thedb_version > sincereaders) stop below the lowest db_version still held by an in-flight transaction.Either needs a test with two sessions, like the reproduction above, and a check of what the chunk tiling and the positional resume assume about commit order.