Repository navigation
Use the raw xmin when looking up a local row's commit timestamp - #674
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Duplication | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
ibrarahmad
left a comment
There was a problem hiding this comment.
The fix is right and matches core: heap_getsysattr() returns HeapTupleHeaderGetRawXmin() for xmin (heaptuple.c:741), and PG 18's conflict.c:84 feeds exactly that to TransactionIdGetCommitTsData(). The non-normal guard still covers rows frozen before 9.4, and the commit_ts lookup returns false with ts = 0 outside the retained range rather than erroring, so the fallback is as described. This is the only place in src/ that read xmin this way. I ran the test on PG 18 with pageinspect installed: 14/14 with the fix, and with the one line reverted test 13 fails with got from_n1, expected from_n2. Two notes on the test inline.
| my $db_password = $config->{db_password}; | ||
|
|
||
| SKIP: { | ||
| skip 'pageinspect extension not available', 1 |
There was a problem hiding this comment.
Without pageinspect this block is skipped and the file still passes, 7 tests instead of 14, so a run on a bare install proves nothing and nothing says so. CI builds contrib, so it ran there (14 tests), but a plan skip_all with the reason, as the injection-point tests do, would make a skipped run visible.
| test: 108_apply_unknown_column_exception | ||
| test: 109_node_info_propagation | ||
| test: 110_resync_node_info_warning | ||
| test: 111_lww_frozen_local_row |
get_tuple_origin() read the local row's xmin with HeapTupleHeaderGetXmin(), which returns FrozenTransactionId once the row is frozen. The non-normal xid branch then set local_ts to 0, so under last_update_wins any incoming UPDATE won against a frozen row, even one older than the local change. Since PostgreSQL 16, VACUUM freezes whole pages early when it is writing a full-page image anyway, so recently written rows can be frozen and nodes can end up with different values. Freezing only sets infomask bits and keeps the original xid, so read it with HeapTupleHeaderGetRawXmin(). This is what core's own conflict detection gets through heap_getsysattr(). Its commit timestamp stays available until commit_ts is truncated; after that the lookup fails and the existing fallback applies. The guard for a non-normal xid stays, for rows frozen before 9.4 and carried over by pg_upgrade. Add TAP test 116_lww_frozen_local_row: an older UPDATE from n1 arrives on n2 after n2 has updated the same row and frozen it; n2 must keep its value.
6b72e69 to
d2d9c4d
Compare
Fixes #672.
Problem
get_tuple_origin()read the local row's xmin withHeapTupleHeaderGetXmin(),which returns
FrozenTransactionIdonce the row is frozen. The non-normal xidbranch then reported a commit timestamp of 0, so under
last_update_winsanyincoming change won against a frozen row, even one older than the local
change. Since PostgreSQL 16, VACUUM freezes whole pages early when it writes a
full-page image anyway, so recently written rows can be frozen and nodes can
end up with different values.
Fix
Freezing only sets infomask bits and keeps the original xid, so read it with
HeapTupleHeaderGetRawXmin(). This is what PostgreSQL's own conflictdetection gets through
heap_getsysattr(). The commit timestamp staysavailable until commit_ts is truncated; after that the lookup fails and the
existing fallback applies. The guard for a non-normal xid stays, for rows
frozen before 9.4 and carried over by pg_upgrade.
Testing
116_lww_frozen_local_row: an older UPDATE from n1 arrives onn2 after n2 has updated the same row and frozen it (confirmed with
pageinspect); n2 must keep its value. Fails without the fix, passes withit.
make regresscheckpasses.