fix: share table-AM parallel scan groups across workers (#1068, rebased + ledger) - #1096
Conversation
phs_nallocated was a first-wins mutex, so launched workers sat idle. Claim it as a group index, the same way the custom scan shares work. Co-authored-by: Cursor <cursoragent@cursor.com>
CI suites (PG 17) refused these checks because a PG18-only seed left majors=18. The suite was run on PGDG 15.19, 16.15, 17.11 and Ubuntu 18.6 and those logs were merged. PG19 is not installed here. Co-authored-by: Cursor <cursoragent@cursor.com>
jdatcmd
left a comment
There was a problem hiding this comment.
Approving. This is the strongest of the five on the merits.
Replacing the first-wins phs_nallocated claim with a per-group claim is the right fix. The old code had one participant take the whole scan and the rest see it exhausted, which was correct but left every launched worker idle. Claiming per group from the same atomic counter is how the custom scan already does it, so the two paths now agree.
Widening gi to uint64 belongs with it: pg_atomic_fetch_add_u64 returns uint64 and the old uint32 would have wrapped the claim counter on a long scan.
The three stale comments that described the single-participant behaviour are all updated, including the one in pgcolumnar_index_build_range_scan, which is the one that would have misled someone debugging a parallel index build.
The ledger correction is load-bearing, proved as on #1092.
Commit preservation checked: every commit from #1068 is an ancestor of this branch.
…t#1068 review) The scan arms drive pgcolumnar_next_group_index through a parallel seq scan. A parallel index build reaches the same shared claim through table_beginscan_parallel, and it was untested -- which matters because it is the consumer where a claim bug is silent. A scan that double-claims returns duplicate rows and someone notices; an index that SKIPS a group is simply missing entries, and every query using it quietly returns fewer rows. THE WORKER COUNT IS NOT A pgcolumnar GUC, and max_parallel_maintenance_workers alone will not produce one. Measured: max_parallel_maintenance_workers = 0 "building index ... serially" max_parallel_maintenance_workers = 2 "with request for 1 parallel workers" max_parallel_maintenance_workers = 8 "with request for 1 parallel workers" + ALTER TABLE ... SET (parallel_workers = 8) "with request for 8 parallel workers" That GUC is a gate, not a dial. Core sizes the request in plan_create_index_workers() from relpages, and a columnar table reports 69 pages for 2,000,000 rows, so the size heuristic grants ONE worker however large the fixture. The table's parallel_workers reloption is the only thing that produces real parallelism here. Both arms say so in a comment, because a bigger table is what the next person will reach for. PROVED IN BOTH DIRECTIONS by mutating the claim stride, a realistic off-by-one in the shared counter: as proposed 14 passed + 0 failed stride 1 -> 2 FAIL a parallel index build indexes every row of the table: got [25000|612512500] want [50000|1250025000] The SUM is doing real work there: a group read twice cancelling a group skipped leaves the count right and the sum wrong. Both harnesses assert the same four names and observe independently -- the shell suite reads PGC_LOGFILE with awk and compares a concatenated string, the pytest twin reads the cluster's own server.log through the pgc_cluster fixture and compares a tuple. Neither invokes the other. compare_to_bash grades them one-for-one: 128/128. Ledger rows re-derived from runs on all five majors rather than by editing the majors field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
…mes-pg19 # Conflicts: # CHANGELOG.md # test/check_ledger_budget.txt # test/pytest/TESTS.md # test/pytest/expected_tests.txt # test/pytest/test_compare_to_bash.py
377a17f to
a45316e
Compare
jdatcmd
left a comment
There was a problem hiding this comment.
Re-approving at a45316e, now covering the index-build arm. My earlier approval was scoped to the scan-sharing C and the ledger, and I said it would not transfer to new code, so this is the review of the arm itself.
The C survived the rebase unchanged
Per-file patch id, approved head 377a17f against this one:
src/columnar_reader.c SAME b1d21867f9ee
src/columnar_tableam.c SAME 36379e7ee60a
So nothing rode in under the bookkeeping.
The vacuous pass was real, and the fix closes it
I reproduced the defect and the repair with the PR's exact expansions rather than taking the run:
OLD form, both reads empty:
got=[] want=[] -> PASS <- the headline lied
NEW form:
both-empty got=[<the index read returned nothing>]
want=[<the sequential read returned nothing>] -> FAIL
idx-empty got=[<the index read returned nothing>]
want=[50000|1250025000] -> FAIL
mismatch got=[25000|612512500] want=[50000|1250025000] -> FAIL
both-present got=[50000|1250025000] want=[50000|1250025000] -> PASS
The sentinels are distinct strings, so they cannot collide on the both-empty path, which is the one that reported PASS before. The pytest twin carries the same shape at lines 262 to 263, so both harnesses fail closed rather than one.
This is the most valuable change in the PR. A headline check that reports PASS having measured nothing is worse than no check, and it was only visible under a mutation that made the build fail rather than return wrong data.
Scoping the claim honestly is the right call
Agreed that the arm tests completeness, not distribution, and that saying so is better than implying more. Entering the parallel path is not claiming groups: pre-#1068 every participant would still enter and all but one would find the scan exhausted, so a PID count proves nothing about sharing.
Distribution is already asserted, on the scan path, by this suite's existing workers share the table-AM scan, it is not a single claimer, which reads per-worker rows= and correctly treats a missing line as no measurement rather than a zero. The index build adds the correctness risk the shared counter introduces, which is the half that was uncovered.
The aggregate rather than a bare count is right: a group read twice and a group skipped cancel in count(*) and do not cancel in sum(id).
Bookkeeping, checked rather than taken
TESTS.md max 46, 0 anomalies in the 1..46 sequence
ledger budget 1249 == actual never-red 1249
majors on new rows 15;16;17;18;19, uniform
cluster_tests 413 -> 415, consistent with the twin's added tests
COMPLETE union preserved, none of main's entries dropped
The union check earned its place twice on this rebase, recovering both native_chunk_length_bound and native_fetch_coalesce.
The relpages note
Thank you for putting plan_create_index_workers, the 69-pages-for-2,000,000-rows measurement, and "the GUC is a gate, the reloption is the dial" in the arm's comment. That is the thing someone debugging a one-worker build reaches for a bigger table over.
Merging once CI is green.
…ences outlive
Four documents said `v1.0-alpha3` was the latest published pre-release. True at
the moment of writing, and false for the entire life of the `v1.0-alpha4` tag that
is cut from this commit.
CHANGELOG.md:10 README.md:27 docs/roadmap.md:16 docs/installation.md:156
The timing is what decides it. The tag is cut FROM this tree, so whatever these
say is what the tag says permanently. Naming alpha4 is wrong for the minutes
between this commit and the tag. Naming alpha3 is wrong for as long as the tag
exists, and README is the first thing a reader opens.
`v1.0-alpha2` and `v1.0-alpha3` both shipped the older form, so the project has
published this three times. That is a reason to stop rather than a reason to
continue: commandprompt#1043 and commandprompt#1088 were both this same class, a document telling a reader
something the tree falsifies, and removing that class is most of two days' work.
Raised by @OffgridwithJD reviewing commandprompt#1099. The half they left alone is the half
about `default_version` being in development, which changes at cycle-open rather
than at the tag. Those read as one sentence and are two claims with two clocks,
so the sentences are now split.
THE CHECK COULD NOT SEE ITS OWN REFERENCE DOCUMENT. `docs_style.sh` matched the
claim in one word order only. Three documents write "latest published pre-release
is `vX`" and CHANGELOG.md writes "`vX` is the latest published pre-release", so
the rule parsed the three copies and skipped the original. It compared the copies
to each other and exempted the one they are copied from.
Both orders are read now, and a file that states the claim in a form the rule
cannot parse is NAMED rather than skipped. Proved by mutation, both directions:
README disagrees FAIL got [DISAGREE:v1.0-alpha3,v1.0-alpha9]
CHANGELOG unparseable FAIL got [CHANGELOG.md]
The agreement check also compared "" against "" and passed when the extraction
came back empty. That is commandprompt#1096's defect exactly, where the premise caught it and
the headline still reported PASS. It carries a distinct sentinel now. Raised by
@OffgridwithJD.
That rule then caught this change: splitting the sentence put the claim across two
lines, and grep is line-based, so two documents became unreadable to it. Rewrapped.
Verification:
docs_style.sh 25 checks, PASSED
plain_language_check.py 0 failures across docs/, README, release notes
test_docs_cover_the_corpus.py 98 checks, 38 passed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
This is @linuxhikerpm's #1068, rebased onto
cfdb393with its ledger rows re-derived. Their commits are preserved; mine is the last one. Opened from my fork because I cannot push to theirs.What I reviewed and found legitimate
This is the strongest of the five on the merits. I did not take the parallel index build on trust — I built one against a 3M-row columnar table and watched it:
pg_stat_activityduring the buildIt works.
What I changed
Only the ledger. Their rows read
15;16;17;18while every other row reads15;16;17;18;19, socovered_majorsincludes 19 and the release gate refuses this suite's ten checks — while CI stays green, becauseci.yml:503runs 17+18 per PR and only the local five-major matrix adds 19.Re-derived from runs, not edited.
Open, not blocking
The parallel index build has no arm. It is the substance of this PR and nothing in the suite would notice if it stopped working — today it is proven by my run, and my run is not in the repo, so it decays the moment either of us stops looking.
An arm need not be elaborate: build an index on a table large enough to recruit workers, then assert the index is complete — the row count through the index matches the row count through a sequential scan. That is the property that matters, because a parallel build that loses rows is the silent failure.
The stronger version also pins that workers were recruited, so the arm cannot pass by quietly building serially — otherwise a change that disables the parallel path leaves it green.
I did not add this myself: it needs a fixture decision (how large, how many workers) that belongs with the author rather than with a reviewer guessing.
Merge order
Takes TESTS.md section 44, correct while main is at 43. All five take 44; the numbering is gated, so the second to land renumbers and re-derives its census — the conflict offers two numbers and neither is right (#996).
Suggested order: #1092 first, then #1095, this, #1094, #1093.
Original: #1068. Author: @linuxhikerpm.