Repository navigation
feat(backup): file-hash incremental detection for heap tables - #98
Conversation
talmacschen-arch
left a comment
There was a problem hiding this comment.
Thanks for this — thoughtful PR and the overall design is sound: with both flags off the path is logically identical to the previous FilterTablesForIncremental, the TOC additions are all omitempty so backward compatibility holds, using a dedicated dbconn to avoid transaction-abort propagation is the right call, and excluding modcount from the AO content hash to dodge GP5's cross-partition modcount propagation addresses a real pain point.
I checked the two issues below against the Cloudberry kernel source and reproduced both on a live cluster, so they are not speculation — details are in the inline comments:
- [correctness]
--heap-file-hashcan silently miss changes on heap tables larger than 1GB — inline ongpbackup_file_info. - [gap]
--heap-file-hashdoes not cover leaf partitions of partitioned heap tables — inline ongetHeapTables.
Two more points:
Test coverage: the new decision logic in FilterTablesForIncremental (AO-hash / heap-hash branches: prev vs current equal → skip, differ → include, either empty → include, Heap == nil → include) has no unit test — the test change only moves the existing flags-off call into JustBeforeEach. These branches are pure map logic over constructed TOCs and can be unit-tested without a live DB; worth adding since this is the heart of the feature.
nits:
ensureFileStatFunctiononlyCREATE OR REPLACEs when the function is absent; an older same-signature(oid)body would not be refreshed.- The plpgsql
EXCEPTION WHEN OTHERS THEN RETURN ''swallows every error (including permission failures) — safe-by-degradation, but hides real errors when debugging. CHECKPOINTis a cluster-wide heavy operation; frequent incrementals add I/O — worth a note in the docs.
Nice work overall — physical-state hashing for change detection is the right direction; both issues are about which physical state is being hashed.
talmacschen-arch
left a comment
There was a problem hiding this comment.
Requesting changes on the strength of the correctness issue in my detailed review above (F1).
On heap tables larger than 1GB, --heap-file-hash stats only the first 1GB segment (pg_relation_filepath never returns the .N tail segments — confirmed in md.c/relpath.c), so append-only changes that land in tail segments are silently missed and dropped from the incremental. I reproduced this on a live cluster: after appending 0.5GB to a >1GB table, the main segment's size|mtime was byte-for-byte unchanged while the data went to .1. That is a data-loss path for the feature.
Please either hash all segments of the relation (walk path, path.1, path.2, … like md.c does) or gate/document the flag for tables over 1GB before merge. F2 (leaf partitions excluded) and the missing unit tests are non-blocking. Happy to re-review once segment handling is addressed — the overall approach is good.
|
@liang8283 heads-up — I've reviewed this PR and left a Request Changes. The one blocker is a correctness issue (F1): on heap tables larger than 1GB, F2 (leaf partitions of partitioned heap tables are excluded via |
woblerr
left a comment
There was a problem hiding this comment.
Preferably splitting --heap-file-hash and --ao-file-hash into two separate PRs. They are independent features, so each can be reviewed and merged on its own.
The --heap-file-hash functionality looks like the most useful change.
|
Thanks both for the careful reviews and the reproductions. I pushed fbd2786, which addresses all of them; details are in the inline replies, and the PR description is updated.
Could you take another look when you have a moment? |
|
A follow-up on
Making it correct would mean adding |
|
@liang8283 The full backup hashes the heap file after CHECKPOINT flushes an uncommitted insert, but COPY cannot see that row. Committing the transaction later leaves the hash unchanged, so the next incremental skips the table. In psql, create a heap table with one committed row, then leave the second insert uncommitted. CREATE TABLE t(id int) USING heap DISTRIBUTED BY (id);
INSERT INTO t VALUES (1);
BEGIN;
INSERT INTO t VALUES (2);In another terminal, run a full backup with --leaf-partition-data --heap-file-hash. Once it finishes, run COMMIT; in the original psql session. Run an incremental from that full backup with --heap-file-hash and restore it. The restore succeeds, but Repeating the incremental from the same full backup with --heap-file-hash disabled restores both 1 and 2. |
|
@woblerr Thanks, confirmed and fixed in b60ef8f. The heap files also contain rows of transactions that are still in progress, and their COMMIT only updates the commit log, so the hash never changed after the commit. The fix: after reading the file hashes, and before the backup snapshot is taken, gpbackup now checks
If the lock check itself fails, no heap hashes are recorded at all. With your exact steps (uncommitted insert during the full backup, committed after it), the incremental now includes |
Thanks, the original case is fixed. Could you check it out, please. |
|
@woblerr Thanks, both confirmed and fixed in 9ef673a. Commit during the wait after With your sequence (heap and AO insert committed in one transaction during the one-second wait), the incremental now includes the heap table and the restore returns the new row in both tables. To reproduce it reliably the row has to land in an existing page; a new row that extends the file moves its mtime immediately and was already caught.
The earlier scenarios (uncommitted writes, tail segments, the lock-wait race, DROP COLUMN, leaf partitions) still behave as before. |
|
@liang8283 Thanks for fixing the previous issues. Create a test database and table. CREATE TYPE pair AS (a int, b int);
CREATE TABLE t(id int, v pair)
USING heap WITH (autovacuum_enabled=false)
DISTRIBUTED BY (id);
INSERT INTO t VALUES (1, ROW(10,20)::pair);
SELECT * FROM t;Take a full backup and note its printed timestamp. Add an attribute to the composite type. ALTER TYPE pair ADD ATTRIBUTE c int CASCADE;
SELECT * FROM t;
-- (10,20,)Take an incremental using the full backup’s timestamp. |
|
@woblerr Thanks, confirmed. Fixed in e90efe1. The enum fix only covered one case of a wider problem: restore loads a skipped table's old data file into the newest column list, and some changes alter that list or how stored values are dumped without touching the data files or the table's DDL timestamp. Besides The enum-label hash is replaced by a hash of the table's own columns (number, name, type, typmod, dropped) and of the definitions of every enum and composite type they use, directly or through domains, arrays, nested composite, range and multirange types, read inside the backup snapshot. The table is skipped only if this hash matches too. The query runs under a savepoint; if it fails, no heap table is skipped. While going over the whole feature again I also fixed a few related cases:
With your steps the incremental now includes |
|
There is one more issue with regclass columns. Could you take a look? With a heap table storing And branch conflicts need to be resolved. |
|
@woblerr Thanks, confirmed. Fixed in 016d651, which also merges
Two smaller changes in the same commit:
With your steps the incremental now includes the referencing table, and the restore returns |
|
@liang8283 Huge work done, thanks! Could you take a look? |
…kups Every incremental backup re-copies every heap table. With the new opt-in --heap-file-hash flag, a heap table is skipped when its on-disk file state, its last DDL timestamp and a hash of its columns and the types they use all match the previous backup. Without the flag, behavior is unchanged. File hash: - A plpgsql helper, gp_toolkit.gpbackup_file_info(oid), runs on every segment and stats every segment file of the main fork (path, path.1, ... until one is missing), so tables larger than RELSEG_SIZE are fully covered. The path is part of the hash, so a new relfilenode (TRUNCATE, VACUUM FULL, CLUSTER) is detected. The helper is created for each hashing step and dropped afterwards. - Hashes are collected on a separate connection after a CHECKPOINT and before the backup snapshot is taken, so a change committed after the snapshot moves the files on by the next backup. - Tables on which another transaction holds a write lock (RowExclusiveLock or stronger) after the hashes are read get no hash, since rows of in-progress transactions are already in the files. - After the snapshot, a second CHECKPOINT and a re-read drop the hash of any table whose files changed in between. - A file modified in the current second gets no hash, since pg_stat_file reports mtime in whole seconds. - Unlogged tables are never hashed: a regular CHECKPOINT does not write their buffers. Columns and types: restore loads a skipped table's old data into the newest column list, so each heap entry also records, read inside the backup snapshot, the table's last DDL timestamp and a hash of its columns and of the enum labels and composite attributes of the types they use, directly or through domains, arrays, composite, range and multirange types. This covers DROP COLUMN, enum RENAME VALUE, composite ADD/DROP ATTRIBUTE, row types used as columns and DROP DOMAIN ... CASCADE. Tables that use regclass and the other reg* types, or aclitem, whose values are dumped as names of the objects they reference, are never hashed. A heap table is skipped only if the previous restore plan has data for it, both backups carry a hash for it, and the file hash, the DDL timestamp and the column hash all match. Whenever a hash cannot be trusted (CHECKPOINT, lock check or column query failure, a missing segment, Greenplum before 6), no hash is recorded and the table is included, as without the flag. --heap-file-hash requires --leaf-partition-data. New TOC fields are omitempty, so older TOCs and older gprestore are unaffected.
98b9792 to
0e007e9
Compare
Summary
Adds an opt-in
--heap-file-hashflag togpbackupso that--incrementalcan skip heap tables whose data has not changed. Today every heap table is re-copied by every incremental backup; with this flag, a heap table is skipped when its on-disk file state, its last DDL timestamp and the definitions of the enum and composite types its columns use all match the previous backup.Without the flag, behavior is unchanged: heap tables are always included, and AO and PAX tables keep using their existing change markers. The flag requires Greenplum 6 or later, or Cloudberry; on Greenplum 5 it logs a warning and no heap table is skipped.
--ao-file-hashfrom the first revision has been dropped: the aoseg content hash does not change on DELETE, so it is not a safe replacement formodcount(see the discussion below).How it works
gp_toolkit.gpbackup_file_info(oid), runs on every segment viagp_dist_random. It resolves the relation's path withpg_relation_filepath()and stats every segment file of the main fork (path,path.1,path.2, ... until one is missing, the same waymd.cwalks them), so tables larger thanRELSEG_SIZEare fully covered. The path itself is part of the hash, so a new relfilenode (TRUNCATE, VACUUM FULL, CLUSTER) is detected as a change.CHECKPOINTso thatpg_stat_filesees flushed data. Any change committed after the snapshot moves the data files on by the next backup'sCHECKPOINT, so the next incremental sees a different hash and includes the table. (Hashing after the snapshot would let changes the backup cannot see enter the hash.)pg_locksand drops the hash of every table on which another transaction holds a write lock (RowExclusiveLockor stronger).CHECKPOINTbut before the snapshot may still sit in shared buffers, invisible to the hash but visible to the backup. Once the snapshot is taken, gpbackup issues a secondCHECKPOINT, re-reads the file info of every hashed table, and drops the hash of any table whose files changed.pg_stat_last_operation, read inside the backup snapshot. A DDL that does not rewrite the table, such asDROP COLUMN, therefore still forces the table into the incremental.ALTER TYPE ... RENAME VALUEon an enum,ALTER TYPE ... ADD/DROP ATTRIBUTEon a composite type, adding a column to a table whose row type another table uses as a column type, orDROP DOMAIN ... CASCADEdropping a column. Each heap entry also records a hash of the table's columns (number, name, type, typmod, dropped) and of the definitions of the enum and composite types they use, directly or through domains, arrays, composite, range and multirange types, read inside the backup snapshot under a savepoint. If it cannot be read, no heap table is skipped.regclassand the otherreg*types, andaclitem, store oids but are dumped as the names of the objects they reference, so renaming or dropping the referenced object changes what COPY writes, or makes the old value fail to load. Tables that use any of these types, directly or through domains, arrays, composite and range types, are never hashed and are always included.CHECKPOINTdoes not write their buffers, so their files may not move when they change.CHECKPOINTfails, or the lock check fails;pg_stat_filereports mtime in whole seconds, so a later write in the same second would not move it);FilterTablesForIncrementalskips a table that is neither AO nor PAX only when the previous restore plan has data for it (a table that belonged to an extension in the previous backup has none), both the previous and the current TOC carry a hash for it, and the hash, the DDL timestamp and the column hash all match.Files changed
toc/toc.goHeapEntry{FileHashMD5, LastDDLTimestamp, ColumnsMD5}andIncrementalEntries.Heap(omitempty).options/flag.go--heap-file-hash.backup/backup.goDoSetupbefore the connection pool takes the snapshot, and verify them after it.backup/queries_incremental.goCollectHeapFileHashes,getTablesWithActiveWriters,VerifyHeapFileHashes,ensureFileStatFunction,getHeapTables,getFileHashesForTables(batched),getHeapIncrementalEntries,getHeapLastDDLTimestamps,getHeapColumnHashes.backup/wrappers.gobackupIncrementalMetadatawrites heap entries (hash, DDL timestamp, column hash) into the TOC.backup/incremental.goFilterTablesForIncrementalskips heap tables viaheapTableUnchanged, only if the previous restore plan has data for them.backup/validate.go--heap-file-hashrequires--leaf-partition-data.backup/incremental_test.goBackward compatibility
omitempty; older TOCs load fine, and oldergprestoreignores the new fields.gp_toolkit.gpbackup_file_infois created only when the flag is used, for the duration of each hashing step, and dropped afterwards, so a stale body is never reused and nothing is left ingp_toolkit. The(text, text)signature from the first revision of this PR is dropped.Operational notes
CHECKPOINTat the start of each backup, plus a one-second wait so files flushed by it are not treated as "modified in the current second", and a secondCHECKPOINTright after the snapshot is taken (usually cheap, since little is dirty by then).--include-table/--exclude-tableare applied afterwards.How tested
make unit: all suites pass, including the newFilterTablesForIncrementalheap tests.--heap-file-hash, then mutations, then an incremental, then a restore of the incremental. Every table's row count matches the source, and:.1grows)CHECKPOINTALTER TABLE ... DROP COLUMNALTER TYPE ... RENAME VALUEon an enum used directly, via a domain array, or via a composite typeALTER TYPE ... ADD ATTRIBUTE/DROP ATTRIBUTE ... CASCADEon a composite type used directly, nested in another composite, or via a domain arrayALTER TABLE ... ADD COLUMNon a table whose row type another heap table uses as a column typeDROP DOMAIN ... CASCADEdropping a column of a heap tableALTER EXTENSION ... DROP TABLE)regclass(directly, in a composite, in a domain array),regprocandaclitemvalues; referenced table and function renamed, or role renamed