Skip to content

fix: share table-AM parallel scan groups across workers (#1068, rebased + ledger) - #1096

Merged
jdatcmd merged 5 commits into
commandprompt:mainfrom
OffgridwithJD:review/1068-ledger-names-pg19
Sep 17, 2026
Merged

jdatcmd merged 5 commits into
commandprompt:mainfrom
OffgridwithJD:review/1068-ledger-names-pg19

Conversation

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

This is @linuxhikerpm's #1068, rebased onto cfdb393 with 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:

  • four workers confirmed live in pg_stat_activity during the build
  • the resulting index complete: every row present, no duplicates, no gaps
  • rescan safe under repeated execution

It works.

What I changed

Only the ledger. Their rows read 15;16;17;18 while every other row reads 15;16;17;18;19, so covered_majors includes 19 and the release gate refuses this suite's ten checks — while CI stays green, because ci.yml:503 runs 17+18 per PR and only the local five-major matrix adds 19.

Re-derived from runs, not edited.

parallel_am_scan on PG15/16/17/18/19:  10/10 each, rc=0
ledger 1240 rows, uniform 15;16;17;18;19, census 1232
GATE rc=0   registered=254 covered=5 not-covered=249 ceiling=249

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.

jdatcmd and others added 3 commits September 17, 2026 00:05
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 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. 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.

OffgridwithJD and others added 2 commits September 17, 2026 17:58
…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
@OffgridwithJD
OffgridwithJD force-pushed the review/1068-ledger-names-pg19 branch from 377a17f to a45316e Compare September 17, 2026 18:04

@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 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.

@jdatcmd
jdatcmd merged commit 162aecf into commandprompt:main Sep 17, 2026
14 checks passed
OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Sep 17, 2026
…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
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