-
Notifications
You must be signed in to change notification settings - Fork 92
Make backfill batch selection exclude rows inserted or updated after backfill start #634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The `WithStateSchema` option allows to specify in which schema the `pgroll` internal state is stored. This is useful when the `pgroll` internal state is stored in a schema other than the default `pgroll`.
Ensure that only rows having an `xmin` that logically follows the `xid` of the transaction that started the backfill are included in the batch.
Add a `WithSchema` option to `backfill.Backfill` to allow specifying the schema in which the backfill will operate.
Ensure rows logically preceding the frozen transaction id for the table are included in the batch of rows to be backfilled.
dbe8bdf
to
4bed314
Compare
I tested this solution with the aid of the Created a Postgres image with the extension installed: FROM gcc:latest AS builder
# Clone the Postgres 17 release branch at depth 1.
# The Postgres version we checkout here must match the version of Postgres run
# in the final image, otherwise there will be a mismatch between the extension
# and the server versions.
WORKDIR /tmp
RUN git clone --branch REL_17_2 --depth=1 https://github.com/postgres/postgres.git
# Install missing build-time dependencies for Postgres
RUN apt update && apt install -y \
make \
flex \
bison
# Run the 'configure' script
WORKDIR postgres
RUN ./configure
# Build the 'xid_wraparound' extension
WORKDIR src/test/modules/xid_wraparound
RUN make
# Build the final image
FROM postgres:17
# Install the extension files from the builder image
RUN mkdir -p /usr/lib/postgresql/17/modules
COPY --from=builder /tmp/postgres/src/test/modules/xid_wraparound/xid_wraparound.so /usr/lib/postgresql/17/lib/
COPY --from=builder /tmp/postgres/src/test/modules/xid_wraparound/xid_wraparound--*.sql /usr/share/postgresql/17/extension/
COPY --from=builder /tmp/postgres/src/test/modules/xid_wraparound/xid_wraparound.control /usr/share/postgresql/17/extension/
# init.sql ensures that the extension is loaded when the container is started
COPY init.sql /docker-entrypoint-initdb.d/
With this it's possible to approach transaction id wraparound quickly by using xids 1 billion at a time: select consume_xids(1_000_000_000);
vacuum verbose <tablename> As expected, once transaction ids surpass 2^32-1, the |
Closing in favor of #652 |
…backfill start (#652) Backfill only rows present at backfill start. This is third approach to solving #583. The previous two are: * #634 * #648 This is the most direct approach to solving the problem. At the same time as the up/down triggers are created to perform a backfill, a `_pgroll_needs_backfill` column is also created on the table to be backfilled. The column has a `DEFAULT` of `true`; the constant default ensures that this extra column can be added quickly without a lengthy `ACCESS_EXCLUSIVE` lock. The column is removed when the the operation is rolled back or completed. The up/down triggers are modified to set `_pgroll_needs_backfill` to false whenever they update a row. The backfill itself is updated to select only rows having `_pgroll_needs_backfill` set to `true` - this ensures that only rows created before the triggers were installed are updated by the backfill. The backfill process still needs to *read* every row in the table, including those inserted/updated after backfill start, but only those rows created before backfill start will be updated. The main disadvantage of this approach is that backfill now requires an extra column to be created on the target table.
Implement one possible solution to limit backfill to touch only rows that existed in the table prior to backfill start and ignore rows inserted or updated after backfill start.
I believe the solution presented here is flawed and should not be merged as is; the PR is open for discussion to see if the technique can be made to work or if we need to take a different approach.
In general, it is fine from a correctness POV if backfill updates a row that was already updated/inserted by a transaction that committed after backfill started; it's not OK from a performance POV however as a backfill running at the same time as a high rate of
INSERT
s into the table will cause the backfill to never terminate (the issue is described in #583).Proposed solution
Backfill works be touching rows in batches (batch size is configurable). 'Touch' in this context means to set the row's PK to itself, causing the already-installed backfill trigger to fire for that row.
As of this PR, the per-batch query looks like:
The first CTE (
WITH batch AS...
) is the relevant part here. The purpose of this CTE is to select the next batch of rows to be updated (and lock those rows for update).The relevant part of the first CTE is this bit:
This is where we attempt to filter out tuples that were created/updated after the backfill process started. '1234' represents the
xid
of when the backfill process started. The first part:checks to see if the tuple is older than the
xid
when the backfill started. If so the tuple should be part of the batch.b_follows_a
implements anxid
-wraparound safe comparison ofxid
s. The transaction id space (0 - 2^32-1) is considered as a circle and anything in the forward half of the circle is considered ahead ofxid
, anything else is behind. See Postgres Internals book - Chapter 7, Freezing for a description:Using this calculation alone to determine relative ages between transaction ids will fail for very old rows (older than 2^31 transactions since backfill start), which will appear to be in the future.
The
frozen_xid
function is defined:The test checks if the transaction id that created the tuple comes before the oldest unfrozen tuple in the table (
pg_class.relfrozenxid
). If so, the tuple is frozen and should be included in the batch even if the visibility check would regard it as in the future.Problem
What happens if a tuple was frozen many billions of transactions ago (ie several
xid
wraparounds ago)? the 32 bitpg_class.relfrozenxid
won't be able to tell us accurately whether thexid
of this extremely old tuple should be considered frozen or not -relfrozenxid
doesn't containepoch
information about which wraparound cycle it refers to.The ultimate truth of a whether a tuple is frozen or not is contained in the tuple header - frozen tuples have their
HEAP_XMIN_FROZEN
bits set int_infomask
:But the only way to access the tuple header is via the
pageinspect
extension. Prior to Postges9.4
, frozen tuples had theirxmin
replaced with a special value to indicate that the row was frozen which made easy identification of frozen tuples from SQL possible, but this is no longer the case - the tupleinfomask
is used instead.Summary
Without a reliable way to check from SQL whether a tuple is frozen, I don't think this approach is robust. Reliably checking whether a tuple is frozen looks like it requires access to the tuple header, not possible from SQL, only by using extensions.
Using
pageinspect
to determine if a tuple is frozen may be possible, but would introduce a dependency on that extension; currentlypgroll
does not require any extensions.Without robust checks for frozen tuples, the backfill process could exclude tuples that should be backfilled potentially resulting in data loss.
References