Skip to content

Fix distributed snapshot race during commit - #1884

Open
hbswcsyzx wants to merge 2 commits into
apache:mainfrom
hbswcsyzx:fix/issue-1465-distributed-snapshot
Open

Fix distributed snapshot race during commit#1884
hbswcsyzx wants to merge 2 commits into
apache:mainfrom
hbswcsyzx:fix/issue-1465-distributed-snapshot

Conversation

@hbswcsyzx

@hbswcsyzxhbswcsyzx commented Aug 5, 2026

Copy link
Copy Markdown

Fixes#1465

What does this PR do?

Fixes a distributed snapshot visibility race during distributed transaction
commit.

The coordinator keeps the distributed GXID in the proc array through the
existing transaction cleanup sequence. Immediately before the first QE commit
notification, it marks the transaction as commitInProgress while holding
ProcArrayLock. When a QD creates a distributed snapshot, it checks these
markers while holding the same lock. If a marked transaction is found, the QD
waits on the existing GXID heavyweight lock, then reacquires the proc-array
lock and builds the snapshot.

This preserves the required ordering:

  • A snapshot that acquires the lock before the marker sees the GXID as in
    progress.
  • A snapshot that acquires the lock after the marker waits until the QE commit
    notifications and normal QD cleanup have completed.
  • ProcArrayEndGxact(), includeInCkpt, the FORGET record, and crash-recovery
    handling keep their existing lifecycle and ordering.

The one-phase path is marked before COMMIT ONE PHASE dispatch, and the
two-phase path is marked before COMMIT PREPARED dispatch, including retries.
WaitForDtxCommit() also reads the published GXID with pg_atomic_read_u64()
to match the existing atomic GXID protocol.

Type of Change

  • Bug fix (non-breaking change)
  • New feature (non-breaking change)
  • Breaking change (fix or feature with breaking changes)
  • Documentation update

Breaking Changes

None.

Test Plan

  • Unit tests added/updated
  • Integration tests added/updated
  • Passed make installcheck
  • Passed make -C src/test installcheck-cbdb-parallel

Targeted validation was performed in Docker using a cluster built from this
branch:

  • issue1465_distributed_snapshot passed: 2/2 isolation2 tests.
  • The baseline reproduced the stale-version result; the fixed build returned
    the committed version for both reads using the same repeatable-read
    snapshot.
  • Partial two-phase commit and repeated phase-2 retry tests kept the reader
    blocked until the transaction completed, then returned the committed value.
  • One-phase commit success and injected QE failure/rollback paths passed.
  • A checkpoint/crash-recovery test recorded the committed GXID, intentionally
    crashed the QD during phase 2, and recovered by re-dispatching COMMIT PREPARED; committed data was retained and no prepared transactions remained
    on either QE.
  • A targeted workload with 16 writers and 8 repeatable-read readers completed
    2,700 writes and 18,948 reader transactions with correct results and no
    remaining prepared transactions.

Impact

Performance:

Snapshot creation can wait during the existing QE commit-notification window.
The wait uses the existing local GXID lock and adds no network round trip.
Snapshots outside that window are unchanged.

User-facing changes:

This prevents distributed snapshots from observing mixed committed and
uncommitted versions of the same distributed update. There are no syntax,
catalog, or API changes.

Dependencies:

None.

Checklist

Additional Context

The earlier approach that removed the GXID from the proc array before QE
notification was superseded. The current patch does not remove or advertise a
GXID early; it adds a commit-in-progress marker and waits at QD distributed
snapshot creation. The code change is limited to the DTX/proc-array paths and
one isolation2 regression test.

CopilotAI lite review requested due to automatic review settings August 5, 2026 05:57

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hi, @hbswcsyzx welcome!🎊 Thanks for taking the effort to make our project better! 🙌 Keep making such awesome contributions!

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses a distributed snapshot visibility race during distributed transaction commit by changing the ordering of GXID/proc-array cleanup vs. QE commit notifications, and adds an isolation2 regression test to reproduce/guard the behavior (issue #1465). In addition, the PR includes a broad set of build/dependency/licensing/documentation updates that are not described in the PR’s stated “fix is contained in …” scope.

Changes:

  • Adjust distributed transaction commit notification ordering so the coordinator clears the distributed GXID from the proc array before dispatching COMMIT/COMMIT PREPARED (retaining a local copy for retries/diagnostics).
  • Add a new isolation2 test + expected output and register it in the isolation2 schedule for issue #1465.
  • Update a variety of build scripts/configuration and add/adjust ASF licensing headers and related files.

Reviewed changes

Copilot reviewed 84 out of 98 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/backend/cdb/cdbtm.cClears proc-array GXID earlier in commit paths; retains GXID locally for later DTX commands/diagnostics.
src/include/cdb/cdbtm.hAdds dtxGxid field to retain the QD GXID after proc-array cleanup.
src/test/isolation2/sql/issue1465_distributed_snapshot.sqlNew isolation test reproducing the distributed snapshot/proc-array race.
src/test/isolation2/expected/issue1465_distributed_snapshot.outExpected output for the new isolation test.
src/test/isolation2/isolation2_scheduleRegisters the new issue1465 isolation test in the schedule.
src/include/crypto/bufenc.hAdds ASF license header block to file header comment.
src/common/sm3.cAdds ASF license header block.
SECURITY.mdAdds ASF license header comment block.
README.mdAdds ASF license header comment block.
README.apache.mdAdds ASF license header comment block.
CONTRIBUTING.mdAdds ASF license header comment block.
CODE_OF_CONDUCT.mdAdds ASF license header comment block.
deploy/README.mdAdds ASF license header comment block.
deploy/build/README.Ubuntu.bashDeletes build helper script.
deploy/build/README.Rhel-Rocky.bashDeletes build helper script.
deploy/build/README.mdDeletes build documentation.
deploy/build/README.macOS.mdDeletes build documentation.
deploy/build/README.macOS.bashDeletes build helper script.
deploy/build/README.Linux.mdDeletes build documentation.
deploy/build/README.CentOS.bashDeletes build helper script.
python-dependencies.txtBumps PyYAML pinned version to 5.4.1.
gpMgmt/bin/MakefileReworks python dependency handling to download sources at build time; adjusts pyyaml/mock build steps.
gpMgmt/bin/pythonSrc/ext/.gitkeepAdds placeholder for pip/curl-downloaded Python source archives.
gpMgmt/bin/.gitignoreIgnores downloaded python source artifacts under pythonSrc/ext.
gpMgmt/test/READMEUpdates doc text to reflect behave now installed via pip instead of vendored docs.
gpMgmt/bin/gpshrinkAdds ASF license header block.
gpMgmt/bin/gpdemoAdds ASF license header block.
gpMgmt/bin/generate-greenplum-path.shAdds “greenplum naming compatibility” notice block.
gpAux/gpdemo/gpdemo-defaults.shAdds ASF license header block.
gpAux/client/scripts/greenplum_clients_path.shAdds “greenplum naming compatibility” notice block.
gpAux/client/install/src/windows/CreatePackage.batSwitches PyYAML installation method to pip-based install.
configure.acUpdates version string; adds AX_COMPARE_VERSION; adds PAX and pythonsrc-ext dependency checks.
configureRegenerated configure script reflecting configure.ac changes (includes PAX dependency/version checks).
config/ax_compare_version.m4Adds Autoconf Archive AX_COMPARE_VERSION macro file.
pom.xmlUpdates project version string; adjusts RAT exclude list organization/entries.
NOTICESimplifies NOTICE contents (removes prior third-party attribution text).
LICENSEReorganizes/updates third-party license attribution sections.
licenses/LICENSE-setuptools.txtDeletes bundled license text file.
licenses/LICENSE-pyyaml.txtDeletes bundled license text file.
licenses/LICENSE-pygresql.txtDeletes bundled license text file.
licenses/LICENSE-bsd3-psutil.txtDeletes bundled license text file.
licenses/LICENSE-bsd2-mock.txtDeletes bundled license text file.
licenses/LICENSE-bsd2-behave.txtDeletes bundled license text file.
.gitmodulesRemoves cpp-stub git submodule entry.
contrib/pgcrypto/sm4.hAdds ASF license header block.
contrib/pax_storage/tools/gen_sql.cReplaces // comment header with ASF-style block header.
contrib/pax_storage/src/cpp/storage/README_CTID_in_pax.mdAdds ASF license header comment block.
contrib/pax_storage/src/cpp/storage/proto/pax.protoAdds ASF license header block comment.
contrib/pax_storage/src/cpp/storage/proto/micro_partition_stats.protoAdds ASF license header block comment.
contrib/pax_storage/src/cpp/storage/pax_test.ccUpdates include path to new in-repo cpp-stub location.
contrib/pax_storage/src/cpp/storage/orc/orc_test.ccUpdates include path to new in-repo cpp-stub location.
contrib/pax_storage/src/cpp/storage/file_system_test.ccUpdates include path to new in-repo cpp-stub location.
contrib/pax_storage/src/cpp/pax_gtest.ccUpdates include path to new in-repo cpp-stub location.
contrib/pax_storage/src/cpp/cpp-stub/src/stub.hAdds vendored cpp-stub header into repo tree.
contrib/pax_storage/src/cpp/cpp-stub/src/addr_pri.hAdds vendored cpp-stub header into repo tree.
contrib/pax_storage/src/cpp/cpp-stub/src_win/addr_any.hAdds vendored cpp-stub Windows header into repo tree.
contrib/pax_storage/src/cpp/cpp-stub/src_darwin/addr_any.hAdds vendored cpp-stub darwin placeholder header into repo tree.
contrib/pax_storage/src/cpp/cpp-stub/README.mdAdds vendored cpp-stub README into repo tree.
contrib/pax_storage/src/cpp/cpp-stub/LICENSEUpdates cpp-stub LICENSE file contents/header.
contrib/pax_storage/src/cpp/cpp-stub/.gitignoreAdds cpp-stub local ignore patterns.
contrib/pax_storage/src/cpp/CMakeLists.txtAdds ASF license header block.
contrib/pax_storage/src/cpp/cmake/pax.cmakeAdds ASF license header block.
contrib/pax_storage/src/cpp/cmake/pax_format.cmakeAdds ASF license header block.
contrib/pax_storage/src/api/python3/test/paxpy_test.pyAdds ASF license header block.
contrib/pax_storage/src/api/python3/setup.pyAdds ASF license header block.
contrib/pax_storage/src/api/python3/setup-debug.pyAdds ASF license header block.
contrib/pax_storage/src/api/python3/paxtype_cast.ccAdds ASF license header block.
contrib/pax_storage/src/api/python3/paxpy_types.hAdds ASF license header block.
contrib/pax_storage/src/api/python3/paxpy_modules.ccAdds ASF license header block.
contrib/pax_storage/src/api/python3/paxpy_comm.hAdds ASF license header block.
contrib/pax_storage/src/api/python3/paxfilereader_type.ccAdds ASF license header block.
contrib/pax_storage/src/api/python3/paxfile_type.ccAdds ASF license header block.
contrib/pax_storage/src/api/python3/CMakeLists.txtAdds ASF license header block.
contrib/pax_storage/MakefileAdds ASF license header block.
contrib/pax_storage/init_fileAdds ASF license header block.
contrib/pax_storage/FindDependencies.cmakeAdds ASF license header block.
contrib/pax_storage/doc/README.toast.mdAdds ASF license header comment block.
contrib/pax_storage/doc/README.mdAdds ASF license header comment block.
contrib/pax_storage/doc/README.format.mdAdds ASF license header comment block.
contrib/pax_storage/doc/README.filter.mdAdds ASF license header comment block.
contrib/pax_storage/doc/README.dev.mdAdds ASF license header comment block.
contrib/pax_storage/doc/README.clustering.mdAdds ASF license header comment block.
contrib/pax_storage/doc/README.catalog.mdAdds ASF license header comment block.
contrib/pax_storage/doc/performance.mdAdds ASF license header comment block.
contrib/pax_storage/CMakeLists.txtAdds ASF license header block.
.github/ISSUE_TEMPLATE/config.ymlAdds ASF header block; changes blank_issues_enabled behavior.
.github/ISSUE_TEMPLATE/bug-report.ymlAdds ASF header block.
.github/DISCUSSION_TEMPLATE/proposal.ymlAdds ASF header block.
.github/DISCUSSION_TEMPLATE/ideas-feature-requests.ymlAdds ASF header block.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadconfigure.ac
Comment on lines +955 to +957
AC_MSG_CHECKING([CMake version])
CMAKE_VERSION=`$CMAKE --version | head -n1 | sed 's/[^0-9]*\([0-9][0-9.]*\).*/\1/'`
AC_MSG_RESULT([$CMAKE_VERSION])
Comment threadconfigure
Comment on lines +9442 to +9446
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking CMake version" >&5
$as_echo_n "checking CMake version... " >&6; }
CMAKE_VERSION=`$CMAKE --version | head -n1 | sed 's/^0-9*\(0-90-9.*\).*/\1/'`
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $CMAKE_VERSION" >&5
$as_echo "$CMAKE_VERSION" >&6; }
Comment on lines +14 to +15
REM Install PyYAML using pip instead of extracting from tarball
pip3 install --target=%GPDB_INSTALL_PATH%\lib\python PyYAML==5.4.1
Comment threadREADME.md
Comment on lines +1 to +5
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
@hbswcsyzx
hbswcsyzxforce-pushed the fix/issue-1465-distributed-snapshot branch from 9440a55 to 8417815CompareAugust 5, 2026 06:19
@zhangwenchao-123

Copy link
Copy Markdown
Contributor

Hi guy, thank you for your contribution, but the pr looks also have some problems as following:
1: It breaks distributed snapshot atomicity.

The current ordering maintains a critical invariant: if a distributed snapshot considers a gxid completed (gxid < xmax and not in inProgressXidArray), the transaction has already committed locally on every QE. This is what makes a distributed read atomic across segments.

With this patch, a snapshot taken after ProcArrayEndGxact() but during the COMMIT PREPARED broadcast treats the gxid as committed while some QEs have not yet processed COMMIT PREPARED. On a QE, the distributed verdict only applies after the local xid is committed (the DistributedLog mapping is written at local commit); on a lagging QE the tuple falls back to local visibility rules, where the xid is still a prepared, in-progress transaction. Consequences:

  • Cross-segment mixed reads: the same snapshot sees the new tuple version on QEs that already committed and the old version on QEs that haven't. The window is the entire phase-2 broadcast, including the retry loop (ResetAllGangs() + retries bounded by dtx_phase2_retry_second), which can last seconds or longer.
  • Non-repeatable reads within a single snapshot: a REPEATABLE READ transaction reading a lagging QE before the commit is applied sees the old version; re-reading with the same snapshot after the QE applies COMMIT PREPARED sees the new version, because the verdict switches from the local fallback to the distributed "committed" verdict.
  • The one-phase path is worse: doNotifyingOnePhaseCommit() runs from prepareDtxTransaction() before the QD writes its own commit record and before the QE has committed anything, so the gxid disappears from snapshots before the transaction is durable anywhere.

2: It breaks checkpoint / crash recovery.

ProcArrayEndGxact() also clears tmGxact->gxid and tmGxact->includeInCkpt. getDtxCheckPointInfo() relies on scanning the proc array for entries with includeInCkpt set to include committed-but-not-yet-forgotten distributed transactions in the checkpoint record, so that crash recovery (recoverTM()) can re-dispatch COMMIT PREPARED to the QEs (see the protocol comment near getDtxCheckPointInfo() in CreateCheckPoint()). Today this flag is cleared only after the FORGET record(doInsertForgetCommitted()).

With this patch, if a checkpoint runs during the broadcast/retry window and the QD then crashes before the FORGET record is written, the committed transaction is missing from the checkpoint's committed-gxid list. Recovery will not re-deliver COMMIT PREPARED, and the prepared transactions on the QEs are treated as in-doubt and rolled back — a durably committed distributed transaction gets partially rolled back on the segments. Committed data is lost.

@hbswcsyzx
hbswcsyzxforce-pushed the fix/issue-1465-distributed-snapshot branch 3 times, most recently from 8af710c to daf0cefCompareAugust 5, 2026 16:37
Keep the QD GXID in the proc array through normal transaction cleanup and mark DTX commits while QE notifications are in progress. QD snapshot creation waits on the existing GXID lock until the coordinator and all QEs have a coherent commit view.
Retain the existing checkpoint and FORGET COMMITTED lifecycle, and add deterministic isolation coverage for two-phase and one-phase commit paths.
Fixesapache#1465
@hbswcsyzx
hbswcsyzxforce-pushed the fix/issue-1465-distributed-snapshot branch from daf0cef to 8967aa4CompareAugust 6, 2026 03:46
Use pg_atomic_read_u64() when WaitForDtxCommit() inspects a DTX.
The GXID is published through atomic_gxid, and a plain 64-bit read
can tear on platforms where 64-bit accesses are not naturally atomic.
Keep the snapshot wait aligned with the existing atomic GXID protocol.
See: Issue#1465 <apache#1465>
@hbswcsyzx

Copy link
Copy Markdown
Author

Thanks for identifying these issues. They exposed a real flaw in the earlier
approach, which removed the GXID from the proc array before QE commit
notification. That approach has been superseded by the current head
4dc2724.

The current fix preserves the original ordering. Before the first QE commit
notification, the QD sets a commitInProgress marker under exclusive
ProcArrayLock. A QD distributed snapshot either acquires the lock before the
marker and records the GXID as in progress, or acquires it after the marker and
waits on the existing GXID heavyweight lock. The wait finishes only after the
QE notifications return and normal QD cleanup reaches the existing proc-array
and lock-release points.

Therefore ProcArrayEndGxact() is not moved, includeInCkpt remains set until
the existing FORGET path, and checkpoint/crash recovery retains its original
GXID list behavior. The one-phase path sets the same marker before
COMMIT ONE PHASE, and the phase-2 marker covers the complete broadcast and
retry loop.

I also changed the wait path to use pg_atomic_read_u64() for atomic_gxid.
Targeted Docker validation covered the isolation2 regression, partial and
retrying phase-2 commits, one-phase commit, checkpoint/crash recovery, and a
16-writer/8-reader repeatable-read workload. The isolation2 test passed 2/2;
the recovery test retained committed data and left no prepared transactions.

Sign up for freeto 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.

[Bug] Concurrent update cause select data error

3 participants

@hbswcsyzx@zhangwenchao-123