pg_dump: Lock all relations, not just plain tables
authorAlvaro Herrera <alvherre@alvh.no-ip.org>
Tue, 27 Oct 2020 17:31:37 +0000 (14:31 -0300)
committerAlvaro Herrera <alvherre@alvh.no-ip.org>
Tue, 27 Oct 2020 17:31:37 +0000 (14:31 -0300)
Now that LOCK TABLE can take any relation type, acquire lock on all
relations that are to be dumped.  This prevents schema changes or
deadlock errors that could cause a dump to fail after expending much
effort.  The server is tested to have the capability and the feature
disabled if it doesn't, so that a patched pg_dump doesn't fail when
connecting to an unpatched server.

Backpatch to 9.5.

Author: Álvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Reported-by: Wells Oliver <wells.oliver@gmail.com>
Discussion: https://postgr.es/m/20201021200659.GA32358@alvherre.pgsql

src/bin/pg_dump/pg_backup.h
src/bin/pg_dump/pg_backup_db.c
src/bin/pg_dump/pg_backup_db.h
src/bin/pg_dump/pg_dump.c

index a6a8e6f2fd864b3887c640e88df00ced00033610..6c79319164144107ccc7cfe5e393798b4ca825d3 100644 (file)
@@ -198,6 +198,8 @@ typedef struct Archive
        int                     minRemoteVersion;       /* allowable range */
        int                     maxRemoteVersion;
 
+       bool            hasGenericLockTable;    /* can LOCK TABLE do non-table rels */
+
        int                     numWorkers;             /* number of parallel processes */
        char       *sync_snapshot_id;   /* sync snapshot id for parallel operation */
 
index 5ba43441f50aa111182ee488bb995efe18f5d617..28b2892538aec63141832c92e2616679687b534d 100644 (file)
@@ -531,6 +531,70 @@ EndDBCopyMode(Archive *AHX, const char *tocEntryTag)
        }
 }
 
+/*
+ * Does LOCK TABLE work on non-table relations on this server?
+ *
+ * Note: assumes it is called out of any transaction
+ */
+bool
+IsLockTableGeneric(Archive *AHX)
+{
+       ArchiveHandle *AH = (ArchiveHandle *) AHX;
+       PGresult *res;
+       char     *sqlstate;
+       bool    retval;
+
+       if (AHX->remoteVersion >= 140000)
+               return true;
+       else if (AHX->remoteVersion < 90500)
+               return false;
+
+       StartTransaction(AHX);
+
+       /*
+        * Try a LOCK TABLE on a well-known non-table catalog; WRONG_OBJECT_TYPE
+        * tells us that this server doesn't support locking non-table rels, while
+        * LOCK_NOT_AVAILABLE and INSUFFICIENT_PRIVILEGE tell us that it does.
+        * Report anything else as a fatal problem.
+        */
+#define ERRCODE_INSUFFICIENT_PRIVILEGE "42501"
+#define ERRCODE_WRONG_OBJECT_TYPE      "42809"
+#define ERRCODE_LOCK_NOT_AVAILABLE     "55P03"
+       res = PQexec(AH->connection,
+                                "LOCK TABLE pg_catalog.pg_class_tblspc_relfilenode_index IN ACCESS SHARE MODE NOWAIT");
+       switch (PQresultStatus(res))
+       {
+               case PGRES_COMMAND_OK:
+                       retval = true;
+                       break;
+               case PGRES_FATAL_ERROR:
+                       sqlstate = PQresultErrorField(res, PG_DIAG_SQLSTATE);
+                       if (strcmp(sqlstate, ERRCODE_WRONG_OBJECT_TYPE) == 0)
+                       {
+                               retval = false;
+                               break;
+                       }
+                       else if (strcmp(sqlstate, ERRCODE_LOCK_NOT_AVAILABLE) == 0 ||
+                                        strcmp(sqlstate, ERRCODE_INSUFFICIENT_PRIVILEGE) == 0)
+                       {
+                               retval = true;
+                               break;
+                       }
+                       /* else, falls through */
+               default:
+                       warn_or_exit_horribly(AH, "LOCK TABLE failed for \"%s\": %s",
+                                                                 "pg_catalog.pg_class_tblspc_relfilenode_index",
+                                                                 PQerrorMessage(AH->connection));
+                       retval = false;         /* not reached */
+                       break;
+       }
+       PQclear(res);
+
+       CommitTransaction(AHX);
+
+       return retval;
+}
+
 void
 StartTransaction(Archive *AHX)
 {
index 8888dd34b9b00994bcd03ab308ec914d9c00139d..355beec1f7c7f6a33dfe030164c898477f3d30ea 100644 (file)
@@ -20,6 +20,8 @@ extern PGresult *ExecuteSqlQueryForSingleRow(Archive *fout, const char *query);
 
 extern void EndDBCopyMode(Archive *AHX, const char *tocEntryTag);
 
+extern bool IsLockTableGeneric(Archive *AHX);
+
 extern void StartTransaction(Archive *AHX);
 extern void CommitTransaction(Archive *AHX);
 
index ff45e3fb8c3797179f71ba377dddea57af9e289b..03f9d4d9e84f723b91af1ba1e3b9ffe23756f244 100644 (file)
@@ -1176,6 +1176,9 @@ setup_connection(Archive *AH, const char *dumpencoding,
                        ExecuteSqlStatement(AH, "SET row_security = off");
        }
 
+       /* Detect whether LOCK TABLE can handle non-table relations */
+       AH->hasGenericLockTable = IsLockTableGeneric(AH);
+
        /*
         * Start transaction-snapshot mode transaction to dump consistent data.
         */
@@ -6843,16 +6846,16 @@ getTables(Archive *fout, int *numTables)
                 * assume our lock on the child is enough to prevent schema
                 * alterations to parent tables.
                 *
-                * NOTE: it'd be kinda nice to lock other relations too, not only
-                * plain or partitioned tables, but the backend doesn't presently
-                * allow that.
-                *
-                * We only need to lock the table for certain components; see
+                * We only need to lock the relation for certain components; see
                 * pg_dump.h
+                *
+                * On server versions that support it, we lock all relations not just
+                * plain tables.
                 */
                if (tblinfo[i].dobj.dump &&
-                       (tblinfo[i].relkind == RELKIND_RELATION ||
-                        tblinfo->relkind == RELKIND_PARTITIONED_TABLE) &&
+                       (fout->hasGenericLockTable ||
+                        tblinfo[i].relkind == RELKIND_PARTITIONED_TABLE ||
+                        tblinfo[i].relkind == RELKIND_RELATION) &&
                        (tblinfo[i].dobj.dump & DUMP_COMPONENTS_REQUIRING_LOCK))
                {
                        resetPQExpBuffer(query);