Skip to content

test: update_conc gets its pytest twin, and a savepoint arm that reaches the subtransaction discard (#1341 item 24) - #1441

Merged
jdatcmd merged 6 commits into
mainfrom
test/1341-update-conc-twin
Oct 16, 2026
Merged

jdatcmd merged 6 commits into
mainfrom
test/1341-update-conc-twin

Conversation

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

#1341 item 24: two transactions UPDATE or DELETE the same row, the loser gets 40001, and no duplicate survives. update_conc.sh had no pytest twin. This PR adds one, every check by the shell's name, and seeds the suite into the ledger. The port found that the suite's savepoint arm cannot reach the code it describes, so a new arm covers that path, red first.

Stacked on #1438. Merge #1438 first; this branch is its head plus three commits.

What changes

  • test/pytest/test_update_conc.py, the twin. It has eleven tests, with each concurrent case running on a heap oracle and on a columnar table.
    • s1 writes and holds its transaction open.
    • s2's statement runs in a thread and must wait on a lock, which a poll of pg_stat_activity (this database only) confirms.
    • s1 then commits.
    • It runs in a private database on the shared cluster, and each session has a 30s lock_timeout, as the shell's cluster does.
    • compare_to_bash.py grades it as covering all 31 shell names (28 checks plus the three bounded-wait names), and it joins COMPLETE.
  • update_conc.sh uses lib.sh's recorders (check, pgc_fail for the timeouts, pgc_summary), so it leaves suites_without_accounting.txt, as unique_conc does in test: unique_conc gets its pytest twin, and its "does not block" arms can fail (#1341 item 23) #1438.
  • The savepoint arm couldn't reach the subtransaction discard. Its inner UPDATE completes, and executor-end flushes the mark and the new row under the inner subtransaction. The abort then hides them, so pgcolumnar_subxact_callback's discards never run.
    • Measured: with either discard removed, both of its arms stay green.
    • The new arm: "savepoint, failed statement". Inside the savepoint, an UPDATE changes id=1 and then fails on id=3 (1/(id-3)). No executor-end runs, so its mark and row are still buffered at ROLLBACK TO. Its premise is the scan order: id=1 is read before id=3.
  • The header says the fix has landed. It used to describe every columnar arm as red on today's code.
  • Ledger: update_conc is seeded with 28 rows from five-major runs, and 14 of the 28 rows dated by the mutants below. suites_not_covered goes 223 → 222, and the census is 3499 + 28 = 3527 rows, 2604 + 14 = 2618 never and 895 + 14 = 909 dated, matching the tsv. The gate gives rc=0 against origin/main.

Red first

Seven mutations, one at a time, on PG18. Each was run through both twins, with the .so rebuilt and its md5 printed every time.

mutation update_conc.sh test_update_conc.py the arms that went red
none (the head) 28/28 28/28 none
PgColumnarSerializeFlushRows returns at once 18/28 5 of 11 tests fail the 10 duplicate-row and 40001 arms of uu/RC, uu/RR, ud/RC, du/RC and multirow (got [2] want [1], got [241] want [200], got [00000] want [40001])
the committed-delete conflict never raised 18/28 5 fail the same 10
the re-read uses GetActiveSnapshot(), not GetLatestSnapshot() 18/28 5 fail the same 10
PgColumnarSerializeRowNow returns at once 27/28 1 fails unique/RC: got [23505|1] want [40001|1], the #1421 bug
the self-modify guard removed 27/28 1 fails self-update: got [2] want [1]
PgColumnarWriteStateDiscardSubXact not called on subxact abort 26/28 1 fails both failed-statement savepoint arms: got [2/10,99|3]
PgColumnarDeleteVectorDiscardSubXact not called 26/28 1 fails the same two: got [0/|1]

Never red (14):

  • the seven heap-oracle arms;
  • the two SERIALIZABLE arms, a regression guard (the loser already got 40001 there before the row lock existed);
  • the two serial controls;
  • the scan-order premise;
  • the completed-UPDATE savepoint arm's two, which neither discard alone reddens.

Runs

major update_conc.sh test_update_conc.py
15 28/28 28/28
16 28/28 28/28
17 28/28 28/28
18 28/28 28/28
19 28/28 28/28

The mutants and the five-major runs were measured on main 6391900. After the restate onto 49457c1, whose only src change is columnar_vector.c (#1436), PG18 is 28/28 in both twins again. The other four majors are re-running and I'll post them here.

Also measured: the guard half (438 tests), harness_selftest 1245/1245 and docs_style 58/58. cluster_tests goes 613 → 624 by collection, and TESTS.md gets §133.

🤖 Generated with Claude Code

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

The four other majors after the restate onto 49457c1, at this head: PG15, 16, 17 and 19 are each 28/28 in both twins. With PG18 from the PR body, all five majors are re-measured on the restated tree.

🤖 Generated with Claude Code

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving 3ac87e5.

At the head, PG17: update_conc.sh 28/28; test_update_conc.py 28 checks, 11 tests. The census at this head (3527 / 2618 / 909) matches the budget paragraph, and suites_not_covered is 222.

Your central claim, with two mutants of my own in pgcolumnar_subxact_callback's SUBXACT_EVENT_ABORT_SUB branch:

mutant shell pytest
PgColumnarWriteStateDiscardSubXact dropped 26/28: both "savepoint, failed statement" arms, got [2/10,99|3] want [1/10|2] its oracle arm, got '2/10,99|3'
PgColumnarDeleteVectorDiscardSubXact dropped 26/28: the same two arms, got [0/|1] want [1/10|2] its oracle arm, got '0/|1'

Each mutant reddens for its own reason. Without the row discard, the failed UPDATE's new row survives as a duplicate (three rows, id=1 twice). Without the mark discard, its delete mark survives and the row is lost. The two older savepoint arms stay green under both, which confirms that they could not reach the discard. I restored the source with a make clean, and git status was clean.

Not a blocker: as in #1438, the pytest test stops at its first failed expect, so it runs 27 checks under a mutant where the shell runs 28.

#1439 and #1442 go first, as agreed. After that this chain restates once, and I'll re-approve its bookkeeping.

jdatcmd and others added 6 commits October 16, 2026 00:37
… can fail (#1341 item 23)

test_unique_conc.py is the twin of unique_conc.sh, every check by the shell's name,
on a private cluster at unique_lock_buckets 100003 (fixed at server start). T1 is
held mid-INSERT by a second row waiting in cq_block; T2 runs in a thread; every
barrier polls pg_stat_activity for a lock wait.

Porting it found four shell arms that could not fail: 3, 4 (differing key), 5
(outside the predicate) and 6 (NULLS DISTINCT). When T2 did block, the server's 60s
lock_timeout aborted it as the 60s bounded wait ran out, and the waiter count read
0: a mutant that hashes only a key's first column left the shell at 33/33. In both
twins T2 now runs those arms and the lock-off controls with a 5s lock_timeout, and
each asserts nothing waits and its INSERT returned in a live transaction. A wait
the next check asserts no longer records a second, fixed-name failure.

The 23505 arms assert the SQLSTATE (psql VERBOSITY verbose), not the index name
alone. The suite uses lib.sh's check, pgc_fail and pgc_summary instead of a local
recorder, so the twin grader can read it and the log has an accounting line. The
PG < 15 skip of NULLS NOT DISTINCT is gone: no supported major takes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ons of columnar_unique.c (#1341)

33 rows from five-major runs (33/33 on each of 15-19). Dated on PG18 by: the per-key
lock never taken (18), the lock taken with the setting off (2), every key in one
bucket (3), only the first key column hashed (1, shared), the partial predicate
ignored (2) and a NULL key locked under NULLS DISTINCT (2). suites_not_covered 224
-> 223; the census is 3507 rows, 2607 never, 900 dated, restated on main 45eaa19.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…th pgc_summary (#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…key (#1341)

conflict() ran the whole race before the test asserted anything. With no key lock
taken, T2 did not wait, T1 then stalled behind T2's uncommitted duplicate, and each
test's first red was "a bounded wait for a standalone sentinel completed", not the
arm the mutant breaks. conflict() now returns the waiter count with T1 held, the
test asserts it, and settle() finishes the race. Measured on PG18: under the
no-lock mutant the six tests now go red on their "serializes" arm (got 0 want 1),
in 62s rather than 422s; on the head, 33/33. The sessions close holder first, so
a T1 still paused when a test stops is released before its own connection closes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hes the subtransaction discard (#1341 item 24)

test_update_conc.py asserts every shell check by name; compare_to_bash grades it as
covering all 31 (28 checks and the three bounded-wait names), and it joins COMPLETE.

update_conc.sh moves onto lib.sh's check, pgc_fail and pgc_summary, so it leaves
suites_without_accounting. Its savepoint arm could not reach the discard it claims:
the inner UPDATE completes, executor-end flushes it under the inner subtransaction,
and with either discard in pgcolumnar_subxact_callback removed it stays green. A new
arm fails a statement partway inside the savepoint, with the scan order asserted as
its premise, and goes red under each (0/|1 and 2/10,99|3).

cluster_tests 627 -> 638 by collection; TESTS.md gets section 135.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tions (#1341)

28 rows from five-major runs (28/28 on each of 15-19). Dated on PG18 by, in
columnar_row_lock.c: the flush-time serialization skipped, the committed-delete
conflict never raised, and the re-read from the statement snapshot (10 arms each,
the same 10); the row lock skipped during a unique-index UPDATE (1); the self-modify
guard removed (1); and in pgcolumnar_subxact_callback, either discard skipped (the
2 failed-statement savepoint arms). suites_not_covered 223 -> 222; the census is
3535 rows, 2621 never, 914 dated, restated on main 45eaa19.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving af72e6a, a bookkeeping-only restate of 3ac87e5, which I approved.

  • Identical per-file patches: update_conc.sh, test_update_conc.py, the ledger rows, test_compare_to_bash.py, TESTS.md (masked) and suites_without_accounting.txt.
  • The census at this head matches the budget paragraph: checks_never_observed_red 2621, and cluster_tests is 638.

I'll merge the chain in order once each head is green.

@jdatcmd
jdatcmd merged commit 6f12e37 into main Oct 16, 2026
15 checks passed
OffgridwithJD pushed a commit that referenced this pull request Oct 22, 2026
… a savepoint leaves nothing (#1341)

test/phase3.sh's savepoint arm rolled back only completed statements.
Executor end had already flushed their buffers under the subtransaction,
so the abort's xmin verdict hid them and the savepoint's own discard was
never reached (#1441). With the subxact abort discarding neither pending
writes nor pending deletes, all 32 checks passed. An INSERT whose 2001st row
divides by zero, and a DELETE that fails at a = 9, are now each rolled back
to a savepoint and must leave nothing, in the transaction and after it.
Measured: the mutants leave 2000 rows visible, and rows 2..8 deleted.

The delete arm said its range straddled a vector boundary and nothing read
one: with 16384-row vectors all 32 passed. The zone maps' maxima are now
read.

The suite moves onto lib.sh's check, as smoke.sh did: its own check called
pgc_record directly, which the twin's grader cannot read.

test/pytest/test_phase3.py is the twin, one check per test by the shell's
name, on a private database. Census 3883 -> 3914 never, 2282 -> 2287
dated, 6165 -> 6201 rows; suites_not_covered 102 -> 101; cluster_tests
2748 -> 2784; TESTS.md section 268.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OffgridwithJD pushed a commit that referenced this pull request Oct 22, 2026
… a savepoint leaves nothing (#1341)

test/phase3.sh's savepoint arm rolled back only completed statements.
Executor end had already flushed their buffers under the subtransaction,
so the abort's xmin verdict hid them and the savepoint's own discard was
never reached (#1441). With the subxact abort discarding neither pending
writes nor pending deletes, all 32 checks passed. An INSERT whose 2001st row
divides by zero, and a DELETE that fails at a = 9, are now each rolled back
to a savepoint and must leave nothing, in the transaction and after it.
Measured: the mutants leave 2000 rows visible, and rows 2..8 deleted.

The delete arm said its range straddled a vector boundary and nothing read
one: with 16384-row vectors all 32 passed. The zone maps' maxima are now
read.

A DELETE whose RETURNING reads the table in a block that commits into
one that aborts must see the rows deleted so far, as heap does. With
the savepoint commit not handing its delete buffers and written marks
to the parent, the abort left the read's marks attributed to a finished
block, no later read wrote them again, and the reads summed 19999
against heap's 14950. The statement's end still wrote every mark, so
only the reads showed it.

The suite moves onto lib.sh's check, as smoke.sh did: its own check called
pgc_record directly, which the twin's grader cannot read.

test/pytest/test_phase3.py is the twin, one check per test by the shell's
name, on a private database. Census 3883 -> 3914 never, 2282 -> 2288
dated, 6165 -> 6202 rows; suites_not_covered 102 -> 101; cluster_tests
2748 -> 2785; TESTS.md section 268.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OffgridwithJD pushed a commit that referenced this pull request Oct 22, 2026
… a savepoint leaves nothing (#1341)

test/phase3.sh's savepoint arm rolled back only completed statements.
Executor end had already flushed their buffers under the subtransaction,
so the abort's xmin verdict hid them and the savepoint's own discard was
never reached (#1441). With the subxact abort discarding neither pending
writes nor pending deletes, all 32 checks passed. An INSERT whose 2001st row
divides by zero, and a DELETE that fails at a = 9, are now each rolled back
to a savepoint and must leave nothing, in the transaction and after it.
Measured: the mutants leave 2000 rows visible, and rows 2..8 deleted.

The delete arm said its range straddled a vector boundary and nothing read
one: with 16384-row vectors all 32 passed. The zone maps' maxima are now
read.

A DELETE whose RETURNING reads the table in a block that commits into
one that aborts must see the rows deleted so far, as heap does. With
the savepoint commit not handing its delete buffers and written marks
to the parent, the abort left the read's marks attributed to a finished
block, no later read wrote them again, and the reads summed 19999
against heap's 14950. The statement's end still wrote every mark, so
only the reads showed it.

The suite moves onto lib.sh's check, as smoke.sh did: its own check called
pgc_record directly, which the twin's grader cannot read.

test/pytest/test_phase3.py is the twin, one check per test by the shell's
name, on a private database. Census 4397 -> 4428 never, 3150 -> 3156
dated, 7547 -> 7584 rows; suites_not_covered 41 -> 40; cluster_tests
3834 -> 3871; TESTS.md section 330.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OffgridwithJD pushed a commit that referenced this pull request Oct 22, 2026
… a savepoint leaves nothing (#1341)

test/phase3.sh's savepoint arm rolled back only completed statements.
Executor end had already flushed their buffers under the subtransaction,
so the abort's xmin verdict hid them and the savepoint's own discard was
never reached (#1441). With the subxact abort discarding neither pending
writes nor pending deletes, all 32 checks passed. An INSERT whose 2001st row
divides by zero, and a DELETE that fails at a = 9, are now each rolled back
to a savepoint and must leave nothing, in the transaction and after it.
Measured: the mutants leave 2000 rows visible, and rows 2..8 deleted.

The delete arm said its range straddled a vector boundary and nothing read
one: with 16384-row vectors all 32 passed. The zone maps' maxima are now
read.

A DELETE whose RETURNING reads the table in a block that commits into
one that aborts must see the rows deleted so far, as heap does. With
the savepoint commit not handing its delete buffers and written marks
to the parent, the abort left the read's marks attributed to a finished
block, no later read wrote them again, and the reads summed 19999
against heap's 14950. The statement's end still wrote every mark, so
only the reads showed it.

The suite moves onto lib.sh's check, as smoke.sh did: its own check called
pgc_record directly, which the twin's grader cannot read.

test/pytest/test_phase3.py is the twin, one check per test by the shell's
name, on a private database. Census 4379 -> 4410 never, 3175 -> 3181
dated, 7554 -> 7591 rows; suites_not_covered 41 -> 40; cluster_tests
3841 -> 3878; TESTS.md section 330.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OffgridwithJD pushed a commit that referenced this pull request Oct 23, 2026
… a savepoint leaves nothing (#1341)

test/phase3.sh's savepoint arm rolled back only completed statements.
Executor end had already flushed their buffers under the subtransaction,
so the abort's xmin verdict hid them and the savepoint's own discard was
never reached (#1441). With the subxact abort discarding neither pending
writes nor pending deletes, all 32 checks passed. An INSERT whose 2001st row
divides by zero, and a DELETE that fails at a = 9, are now each rolled back
to a savepoint and must leave nothing, in the transaction and after it.
Measured: the mutants leave 2000 rows visible, and rows 2..8 deleted.

The delete arm said its range straddled a vector boundary and nothing read
one: with 16384-row vectors all 32 passed. The zone maps' maxima are now
read.

A DELETE whose RETURNING reads the table in a block that commits into
one that aborts must see the rows deleted so far, as heap does. With
the savepoint commit not handing its delete buffers and written marks
to the parent, the abort left the read's marks attributed to a finished
block, no later read wrote them again, and the reads summed 19999
against heap's 14950. The statement's end still wrote every mark, so
only the reads showed it.

The suite moves onto lib.sh's check, as smoke.sh did: its own check called
pgc_record directly, which the twin's grader cannot read.

test/pytest/test_phase3.py is the twin, one check per test by the shell's
name, on a private database. Census 4434 -> 4465 never, 3329 -> 3335
dated, 7763 -> 7800 rows; suites_not_covered 35 -> 34; cluster_tests
4035 -> 4072; TESTS.md section 335.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants