From e9c03c3b1b6d14e48fa1cfbb5831a9a03a0c0ab7 Mon Sep 17 00:00:00 2001 From: Tom Lane <tgl@sss.pgh.pa.us> Date: Thu, 2 Dec 2004 19:28:49 +0000 Subject: [PATCH] Disallow the combination VACUUM FULL FREEZE for safety's sake, for the reasons I outlined in pghackers a few days ago. Also, undo someone's overly optimistic decision to reduce tuple state checks from if (...) elog() to Asserts. If I trusted this code more, I might think it was a good idea to disable these checks in production installations. But I don't. --- doc/src/sgml/manage-ag.sgml | 5 +- doc/src/sgml/ref/vacuum.sgml | 6 +- src/backend/commands/vacuum.c | 139 ++++++++++++++++++++++------------ 3 files changed, 95 insertions(+), 55 deletions(-) diff --git a/doc/src/sgml/manage-ag.sgml b/doc/src/sgml/manage-ag.sgml index 0237e026b19..5f01b66ac70 100644 --- a/doc/src/sgml/manage-ag.sgml +++ b/doc/src/sgml/manage-ag.sgml @@ -1,5 +1,5 @@ <!-- -$PostgreSQL: pgsql/doc/src/sgml/manage-ag.sgml,v 2.36 2004/11/05 19:15:49 tgl Exp $ +$PostgreSQL: pgsql/doc/src/sgml/manage-ag.sgml,v 2.37 2004/12/02 19:28:48 tgl Exp $ --> <chapter id="managing-databases"> @@ -240,8 +240,7 @@ createdb -T template0 <replaceable>dbname</> <para> After preparing a template database, or making any changes to one, - it is a good idea to perform - <command>VACUUM FREEZE</> or <command>VACUUM FULL FREEZE</> in that + it is a good idea to perform <command>VACUUM FREEZE</> in that database. If this is done when there are no other open transactions in the same database, then it is guaranteed that all rows in the database are <quote>frozen</> and will not be subject to transaction diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml index 930ce7e3bac..e020c9a9acd 100644 --- a/doc/src/sgml/ref/vacuum.sgml +++ b/doc/src/sgml/ref/vacuum.sgml @@ -1,5 +1,5 @@ <!-- -$PostgreSQL: pgsql/doc/src/sgml/ref/vacuum.sgml,v 1.35 2004/03/23 13:21:41 neilc Exp $ +$PostgreSQL: pgsql/doc/src/sgml/ref/vacuum.sgml,v 1.36 2004/12/02 19:28:48 tgl Exp $ PostgreSQL documentation --> @@ -20,8 +20,8 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ <replaceable class="PARAMETER">table</replaceable> ] -VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] ANALYZE [ <replaceable class="PARAMETER">table</replaceable> [ (<replaceable class="PARAMETER">column</replaceable> [, ...] ) ] ] +VACUUM [ FULL | FREEZE ] [ VERBOSE ] [ <replaceable class="PARAMETER">table</replaceable> ] +VACUUM [ FULL | FREEZE ] [ VERBOSE ] ANALYZE [ <replaceable class="PARAMETER">table</replaceable> [ (<replaceable class="PARAMETER">column</replaceable> [, ...] ) ] ] </synopsis> </refsynopsisdiv> diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index 941e0d36e66..93694e4960f 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -13,7 +13,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/commands/vacuum.c,v 1.296 2004/12/01 19:00:41 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/commands/vacuum.c,v 1.297 2004/12/02 19:28:49 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -265,6 +265,27 @@ vacuum(VacuumStmt *vacstmt) else in_outer_xact = IsInTransactionChain((void *) vacstmt); + /* + * Disallow the combination VACUUM FULL FREEZE; although it would mostly + * work, VACUUM FULL's ability to move tuples around means that it is + * injecting its own XID into tuple visibility checks. We'd have to + * guarantee that every moved tuple is properly marked XMIN_COMMITTED or + * XMIN_INVALID before the end of the operation. There are corner cases + * where this does not happen, and getting rid of them all seems hard + * (not to mention fragile to maintain). On the whole it's not worth it + * compared to telling people to use two operations. See pgsql-hackers + * discussion of 27-Nov-2004, and comments below for update_hint_bits(). + * + * Note: this is enforced here, and not in the grammar, since (a) we can + * give a better error message, and (b) we might want to allow it again + * someday. + */ + if (vacstmt->vacuum && vacstmt->full && vacstmt->freeze) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("VACUUM FULL FREEZE is not supported"), + errhint("Use VACUUM FULL, then VACUUM FREEZE."))); + /* * Send info about dead objects to the statistics collector */ @@ -1346,8 +1367,7 @@ scan_heap(VRelStats *vacrelstats, Relation onerel, do_shrinking = false; break; default: - /* unexpected HeapTupleSatisfiesVacuum result */ - Assert(false); + elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); break; } @@ -1530,9 +1550,7 @@ repair_frag(VRelStats *vacrelstats, Relation onerel, VacPageList vacuum_pages, VacPageList fraged_pages, int nindexes, Relation *Irel) { -#ifdef USE_ASSERT_CHECKING TransactionId myXID = GetCurrentTransactionId(); -#endif Buffer dst_buffer = InvalidBuffer; BlockNumber nblocks, blkno; @@ -1702,36 +1720,30 @@ repair_frag(VRelStats *vacrelstats, Relation onerel, */ if (!(tuple.t_data->t_infomask & HEAP_XMIN_COMMITTED)) { + if (tuple.t_data->t_infomask & HEAP_MOVED_IN) + elog(ERROR, "HEAP_MOVED_IN was not expected"); + if (!(tuple.t_data->t_infomask & HEAP_MOVED_OFF)) + elog(ERROR, "HEAP_MOVED_OFF was expected"); + /* - * There cannot be another concurrently running VACUUM. If - * the tuple had been moved in by a previous VACUUM, the - * visibility check would have set XMIN_COMMITTED. If the - * tuple had been moved in by the currently running - * VACUUM, the loop would have been terminated. We had - * elog(ERROR, ...) here, but as we are testing for a - * can't-happen condition, Assert() seems more - * appropriate. + * MOVED_OFF by another VACUUM would have caused the + * visibility check to set XMIN_COMMITTED or XMIN_INVALID. */ - Assert(!(tuple.t_data->t_infomask & HEAP_MOVED_IN)); + if (HeapTupleHeaderGetXvac(tuple.t_data) != myXID) + elog(ERROR, "invalid XVAC in tuple header"); /* * If this (chain) tuple is moved by me already then I * have to check is it in vacpage or not - i.e. is it * moved while cleaning this page or some previous one. */ - Assert(tuple.t_data->t_infomask & HEAP_MOVED_OFF); - - /* - * MOVED_OFF by another VACUUM would have caused the - * visibility check to set XMIN_COMMITTED or XMIN_INVALID. - */ - Assert(HeapTupleHeaderGetXvac(tuple.t_data) == myXID); /* Can't we Assert(keep_tuples > 0) here? */ if (keep_tuples == 0) continue; - if (chain_tuple_moved) /* some chains was moved while */ - { /* cleaning this page */ + if (chain_tuple_moved) + { + /* some chains were moved while cleaning this page */ Assert(vacpage->offsets_free > 0); for (i = 0; i < vacpage->offsets_free; i++) { @@ -2133,15 +2145,19 @@ repair_frag(VRelStats *vacrelstats, Relation onerel, continue; /* - * * See comments in the walk-along-page loop above, why - * we * have Asserts here instead of if (...) elog(ERROR). + * See comments in the walk-along-page loop above about + * why only MOVED_OFF tuples should be found here. */ - Assert(!(htup->t_infomask & HEAP_MOVED_IN)); - Assert(htup->t_infomask & HEAP_MOVED_OFF); - Assert(HeapTupleHeaderGetXvac(htup) == myXID); + if (htup->t_infomask & HEAP_MOVED_IN) + elog(ERROR, "HEAP_MOVED_IN was not expected"); + if (!(htup->t_infomask & HEAP_MOVED_OFF)) + elog(ERROR, "HEAP_MOVED_OFF was expected"); + if (HeapTupleHeaderGetXvac(htup) != myXID) + elog(ERROR, "invalid XVAC in tuple header"); + if (chain_tuple_moved) { - /* some chains was moved while cleaning this page */ + /* some chains were moved while cleaning this page */ Assert(vacpage->offsets_free > 0); for (i = 0; i < vacpage->offsets_free; i++) { @@ -2294,7 +2310,13 @@ repair_frag(VRelStats *vacrelstats, Relation onerel, vacrelstats->rel_tuples, keep_tuples); } - /* clean moved tuples from last page in Nvacpagelist list */ + /* + * Clean moved-off tuples from last page in Nvacpagelist list. + * + * We need only do this in this one page, because higher-numbered + * pages are going to be truncated from the relation entirely. + * But see comments for update_hint_bits(). + */ if (vacpage->blkno == (blkno - 1) && vacpage->offsets_free > 0) { @@ -2324,16 +2346,18 @@ repair_frag(VRelStats *vacrelstats, Relation onerel, continue; /* - * * See comments in the walk-along-page loop above, why - * we * have Asserts here instead of if (...) elog(ERROR). + * See comments in the walk-along-page loop above about + * why only MOVED_OFF tuples should be found here. */ - Assert(!(htup->t_infomask & HEAP_MOVED_IN)); - Assert(htup->t_infomask & HEAP_MOVED_OFF); - Assert(HeapTupleHeaderGetXvac(htup) == myXID); + if (htup->t_infomask & HEAP_MOVED_IN) + elog(ERROR, "HEAP_MOVED_IN was not expected"); + if (!(htup->t_infomask & HEAP_MOVED_OFF)) + elog(ERROR, "HEAP_MOVED_OFF was expected"); + if (HeapTupleHeaderGetXvac(htup) != myXID) + elog(ERROR, "invalid XVAC in tuple header"); itemid->lp_flags &= ~LP_USED; num_tuples++; - } Assert(vacpage->offsets_free == num_tuples); @@ -2644,20 +2668,36 @@ move_plain_tuple(Relation rel, /* * update_hint_bits() -- update hint bits in destination pages * - * Scan all the pages that we moved tuples onto and update tuple - * status bits. This is not really necessary, but will save time for - * future transactions examining these tuples. + * Scan all the pages that we moved tuples onto and update tuple status bits. + * This is normally not really necessary, but it will save time for future + * transactions examining these tuples. + * + * This pass guarantees that all HEAP_MOVED_IN tuples are marked as + * XMIN_COMMITTED, so that future tqual tests won't need to check their XVAC. + * + * BUT NOTICE that this code fails to clear HEAP_MOVED_OFF tuples from + * pages that were move source pages but not move dest pages. The bulk + * of the move source pages will be physically truncated from the relation, + * and the last page remaining in the rel will be fixed separately in + * repair_frag(), so the only cases where a MOVED_OFF tuple won't get its + * hint bits updated are tuples that are moved as part of a chain and were + * on pages that were not either move destinations nor at the end of the rel. + * To completely ensure that no MOVED_OFF tuples remain unmarked, we'd have + * to remember and revisit those pages too. * - * XXX NOTICE that this code fails to clear HEAP_MOVED_OFF tuples from - * pages that were move source pages but not move dest pages. One - * also wonders whether it wouldn't be better to skip this step and - * let the tuple status updates happen someplace that's not holding an - * exclusive lock on the relation. + * Because of this omission, VACUUM FULL FREEZE is not a safe combination; + * it's possible that the VACUUM's own XID remains exposed as something that + * tqual tests would need to check. + * + * For the non-freeze case, one wonders whether it wouldn't be better to skip + * this work entirely, and let the tuple status updates happen someplace + * that's not holding an exclusive lock on the relation. */ static void update_hint_bits(Relation rel, VacPageList fraged_pages, int num_fraged_pages, BlockNumber last_move_dest_block, int num_moved) { + TransactionId myXID = GetCurrentTransactionId(); int checked_moved = 0; int i; VacPage *curpage; @@ -2696,12 +2736,13 @@ update_hint_bits(Relation rel, VacPageList fraged_pages, int num_fraged_pages, continue; /* - * See comments in the walk-along-page loop above, why we have - * Asserts here instead of if (...) elog(ERROR). The - * difference here is that we may see MOVED_IN. + * Here we may see either MOVED_OFF or MOVED_IN tuples. */ - Assert(htup->t_infomask & HEAP_MOVED); - Assert(HeapTupleHeaderGetXvac(htup) == GetCurrentTransactionId()); + if (!(htup->t_infomask & HEAP_MOVED)) + elog(ERROR, "HEAP_MOVED_OFF/HEAP_MOVED_IN was expected"); + if (HeapTupleHeaderGetXvac(htup) != myXID) + elog(ERROR, "invalid XVAC in tuple header"); + if (htup->t_infomask & HEAP_MOVED_IN) { htup->t_infomask |= HEAP_XMIN_COMMITTED; -- GitLab