Skip to content

Use GetLatestSnapshot in CleanupRecordExists (#8594) - #8709

Merged
ibrahim halatci (ihalatci) merged 1 commit into
release-13.2from
ihalatci-bp-8594-release-13-2
Aug 4, 2026
Merged

Use GetLatestSnapshot in CleanupRecordExists (#8594)#8709
ibrahim halatci (ihalatci) merged 1 commit into
release-13.2from
ihalatci-bp-8594-release-13-2

Conversation

@ihalatci

Copy link
Copy Markdown
Contributor

DESCRIPTION: Prevent a possible race condition in shard cleanup

Backport of #8594 to release-13.2.
Tracks #8707.

systable_beginscan without an explicit snapshot may use a cached catalog
snapshot. If an operation deletes its cleanup record just before the cleaner
acquires the operation-ID lock, CleanupRecordExists could still see the stale
row and incorrectly proceed to drop the underlying resource.

Cherry-picked from c41586cc8fa6e96a677b921c01be6f585f27d819.

Adaptation

Clean apply, no adaptation. The stable patch ID is identical to upstream.

Behavior-change analysis

  • CleanupRecordExists is static and has exactly one call site, in
    DropOrphanedResourcesForCleanup, immediately after a successful
    TryLockOperationId().
  • TryLockOperationId() acquires the operation-ID exclusive lock with
    dontWait = true; an active operation owner therefore causes the cleaner to
    skip the record.
  • The fresher snapshot can only expose a cleanup-record deletion committed
    after the original list was read. It can only prevent the stale-row drop
    race; no other caller inherits the changed snapshot semantics.

Verification (PostgreSQL 17.10)

  • Clean build with ./configure --without-libcurl.
  • check-operations: all 17 tests passed.
  • Full check-multi A/B against pristine release-13.2: all 190 test statuses
    were identical. Both runs had the same pre-existing local-environment failure,
    custom_aggregate_support, because TopN was installed while the selected
    expected variant assumes it is absent. The normalized status files have the
    same SHA-256, and the failing result files are byte-identical.

DESCRIPTION: Prevent a possible race condition in shard cleanup

systable_beginscan without a snapshot calls GetCatalogSnapshot, which
might be cached from a previous call. That means that if a shard move
succeeded and the deletion of the cleanup record completed just before
the cleaner tries to acquire a lock, `CleanupRecordExists` might still
see the record in its snapshot and shard cleanup will incorrectly
proceed.

---------

Co-authored-by: Colm McHugh <colm.mchugh@gmail.com>
(cherry picked from commit c41586c)
@ihalatci
ibrahim halatci (ihalatci) merged commit f289638 into release-13.2 Aug 4, 2026
172 of 210 checks passed
@ihalatci
ibrahim halatci (ihalatci) deleted the ihalatci-bp-8594-release-13-2 branch August 4, 2026 13:07
ibrahim halatci (ihalatci) added a commit that referenced this pull request Aug 4, 2026
…8714)

Backport of #8594 to `release-14.0`. Tracking issue: #8713.

**Verbatim cherry-pick** — `git cherry-pick -x c41586c`, zero
conflicts, **zero adaptation**. `git patch-id --stable` is identical to
the upstream commit.

### The bug
`CleanupRecordExists` ran its catalog lookup under whatever snapshot the
caller happened to hold. In the shard-cleanup path that snapshot can
predate the deletion of a cleanup record, so an already-deleted record
still looks present — and cleanup then proceeds to drop a shard that is
actually live.

### The fix
Take a fresh snapshot for the lookup:

```c
Snapshot snapshot = GetLatestSnapshot();
```

### Why this cannot change behavior on 14.0
`CleanupRecordExists` is `static` with exactly **one** call site, and
that site is reached only after a successful `TryLockOperationId()`. A
fresher snapshot can only ever flip the result `true → false` — i.e. it
can only *prevent* an erroneous drop. It cannot cause a drop that the
old code would have skipped.

### Applicability to release-14.0
Vulnerable code confirmed present before the pick: `shard_cleaner.c:1183
CleanupRecordExists(uint64 recordId)` — the old signature with no
snapshot argument. `git diff c41586c^ release-14.0` over the touched
file was empty, i.e. the branch was in the exact pre-fix state.

### Validation
Branch is 1 ahead / 0 behind `8cdb17e25`.

Full A/B on PG16.14 (WSL), unmodified `release-14.0` vs a stack of all
five backports, **with `make install` before each leg and an assertion
that the installed `citus.so` md5 matches the worktree build**:

| leg | `check-multi` | `check-multi-1` |
|---|---|---|
| baseline `8cdb17e25` | 190 ok, 0 failed | 207 ok, 0 failed |
| all five stacked | 190 ok, 0 failed | 207 ok, 0 failed |

Normalized status sets are **byte-identical** between the two legs
(`check-multi` SHA256 `d94f42d7…`, `check-multi-1` SHA256 `f47f18bf…`),
with zero `not ok` lines on either side.

Also swept: `release-14.0` has **no** `_N` numbered expected-output
variants anywhere, so there is no PG-version-variant exposure of the
kind that required a five-line adaptation when #8556 was backported to
`release-13.2`.

Same fix shipped in **12.1.14** via #8703 and proposed for
`release-13.2` in #8709.

Co-authored-by: Marco Slot <marco.slot@snowflake.com>
Co-authored-by: Colm McHugh <colm.mchugh@gmail.com>
ibrahim halatci (ihalatci) pushed a commit that referenced this pull request Aug 5, 2026
Adds the CHANGELOG entry for v13.4.0.

This release includes the following changes merged into release-13.2
since v13.3.0:
- #8621 - citus_internal.distribute_object() repair UDF
- #8625 - citus.allow_unsafe_insert_select_pushdown GUC
- #8566 (via #8695) - skip 2PC for single-statement single-shard procedures
- #8587 - object ownership checks for more citus-internal UDFs
- #8638 (via #8654) - re-range sequences when promoting a clone node
- #8651 (via #8657) - drop orphaned Citus local table shard copies
- #8497 - wrong results when recursive planning projects columns as NULL
- #8692 - UPDATE/DELETE with shard key equality and always-false predicate
- #8594 (via #8709) - race condition in shard cleanup
- #8498 (via #8710) - type mismatch with COLLATE and type cast
- #8556 (via #8711) - segfault in EXPLAIN with LEFT JOIN and subqueries
- #8561 (via #8712) - crash on writable standby coordinator

Version bump to 13.4.0 was already done in 0aa073f.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61fba89e-b958-4d9f-902e-eb76004b5580
ibrahim halatci (ihalatci) pushed a commit that referenced this pull request Aug 6, 2026
Adds the CHANGELOG entry for v13.4.0.

This release includes the following changes merged into release-13.2
since v13.3.0:
- #8621 - citus_internal.distribute_object() repair UDF
- #8625 - citus.allow_unsafe_insert_select_pushdown GUC
- #8566 (via #8695) - skip 2PC for single-statement single-shard procedures
- #8587 - object ownership checks for more citus-internal UDFs
- #8638 (via #8654) - re-range sequences when promoting a clone node
- #8651 (via #8657) - drop orphaned Citus local table shard copies
- #8497 - wrong results when recursive planning projects columns as NULL
- #8692 - UPDATE/DELETE with shard key equality and always-false predicate
- #8594 (via #8709) - race condition in shard cleanup
- #8498 (via #8710) - type mismatch with COLLATE and type cast
- #8556 (via #8711) - segfault in EXPLAIN with LEFT JOIN and subqueries
- #8561 (via #8712) - crash on writable standby coordinator

Version bump to 13.4.0 was already done in 0aa073f.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61fba89e-b958-4d9f-902e-eb76004b5580
ibrahim halatci (ihalatci) added a commit that referenced this pull request Aug 6, 2026
Adds the CHANGELOG entry for **v13.4.0**.

## This is a CHANGELOG-only release

The version bump was already landed by `0aa073fe4` ("Set 13.4.0 as
upcoming version; run N-1 tests against 13.3.0", #8632). Re-verified on
this branch, all already at 13.4.0 / 13.4-1:

| artifact | state |
|---|---|
| `configure.ac` | `AC_INIT([Citus], [13.4.0])` |
| `configure` | 9 version strings at `13.4.0` |
| `src/backend/distributed/citus.control` | `default_version = '13.4-1'`
|
| `src/backend/columnar/citus_columnar.control` | `default_version =
'13.4-1'` |
| `sql/citus--13.3-1--13.4-1.sql` + downgrade | present |
| `sql/citus_columnar--13.3-1--13.4-1.sql` + downgrade | present |
| `expected/multi_extension.out` | contains the 13.4-1 tests |

So this PR touches **one file**: `CHANGELOG.md`, 44 insertions, 0
deletions.

This mirrors the shape of the v13.3.0 release commit `e5b5a3c45`
(#8588), which was likewise CHANGELOG-only because its bump had landed
in `e8a841d46`.

I also checked the one-off cleanup that `e5b5a3c45` carried: it removed
4 accidentally duplicated `DROP FUNCTION` lines from
`citus--13.3-1--13.2-1.sql`. The 13.4 counterpart
`citus--13.4-1--13.3-1.sql` has no duplicates, so no SQL cleanup is
needed here.

## Contents

`v13.3.0..release-13.2` contains 14 non-merge commits; 12 are
changelogged. The two excluded:

- `0aa073fe4` - version housekeeping (referenced in the commit message
instead)
- `f5b93d328` - PG-version/N-1 expected-output fixup that folds into
#8625

Citations use **origin PR numbers**, with `(via #backportPR)` noted in
the commit message only where they differ - matching the v13.3.0
convention (its bullet for #8466 reads just `(#8466)` even though the
commit body says "via #8579").

| origin PR | via | entry |
|---|---|---|
| #8621 | - | `citus_internal.distribute_object()` repair UDF |
| #8625 | - | `citus.allow_unsafe_insert_select_pushdown` GUC |
| #8566 | #8695 | skip 2PC for single-statement single-shard procedures
|
| #8587 | - | object ownership for more citus-internal UDFs |
| #8638 | #8654 | re-range sequences when promoting a clone node |
| #8651 | #8657 | drop orphaned Citus local table shard copies |
| #8497 | - | wrong results when recursive planning projects columns as
NULL |
| #8692 | - | `UPDATE`/`DELETE` with shard key equality + always-false
predicate |
| #8594 | #8709 | race condition in shard cleanup |
| #8498 | #8710 | type mismatch with `COLLATE` + type cast |
| #8556 | #8711 | segfault in `EXPLAIN` with `LEFT JOIN` + correlated
subqueries |
| #8561 | #8712 | crash on writable standby coordinator |

The last four are the 12.1/13.2/14.0 P1 backport set tracked in #8707.

## Formatting

Verified against the existing sections: header `### citus v13.4.0
(August 4, 2026) ###`, all lines <= 76 columns, 2-space continuation
indent, blank line between every bullet, `Adds` entries before `Fixes`.

Note this repo has `core.autocrlf=true`, so the worktree is CRLF while
the blob is LF. The section was written with CRLF and the committed blob
verified LF-only (`git ls-files --eol` -> `i/lf w/crlf`), which is why
the diff is a clean 44-line insertion rather than a whole-file rewrite.

Co-authored-by: Ibrahim Halatci <ihalatci@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61fba89e-b958-4d9f-902e-eb76004b5580
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