Skip to content

PostgreSQL: concurrent transactions can take the same db_version, and a download then skips one of them #79

Description

@andinux

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions