Add parallelism and glibc dependent only options to reindexdb
Hi,
With the glibc 2.28 coming, all users will have to reindex almost
every indexes after a glibc upgrade to guarantee the lack of
corruption. Unfortunately, reindexdb is not ideal for that as it's
processing everything using a single connexion and isn't able to
discard indexes that doesn't depend on a glibc collation.
PFA a patchset to add parallelism to reindexdb (reusing the
infrastructure in vacuumdb with some additions) and an option to
discard indexes that doesn't depend on glibc (without any specific
collation filtering or glibc version detection), with updated
regression tests. Note that this should be applied on top of the
existing reindexdb cleanup & refactoring patch
(https://commitfest.postgresql.org/23/2115/).
This was sponsored by VMware, and has been discussed internally with
Kevin and Michael, in Cc.
Attachments:
0001-Export-vacuumdb-s-parallel-infrastructure.patchapplication/octet-stream; name=0001-Export-vacuumdb-s-parallel-infrastructure.patchDownload
From 92fd2396d8e4dc68b1030802a7c549f60d9554ac Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 28 Jun 2019 13:11:29 +0200
Subject: [PATCH 1/4] Export vacuumdb's parallel infrastructure
---
src/bin/scripts/Makefile | 4 +-
src/bin/scripts/parallel.c | 282 ++++++++++++++++++++++++++++++++++++
src/bin/scripts/parallel.h | 34 +++++
src/bin/scripts/vacuumdb.c | 284 +------------------------------------
4 files changed, 319 insertions(+), 285 deletions(-)
create mode 100644 src/bin/scripts/parallel.c
create mode 100644 src/bin/scripts/parallel.h
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 9f352b5e2b..6979d8f9ff 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -28,7 +28,7 @@ createuser: createuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport
dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-vacuumdb: vacuumdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+vacuumdb: vacuumdb.o common.o parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
@@ -50,7 +50,7 @@ uninstall:
clean distclean maintainer-clean:
rm -f $(addsuffix $(X), $(PROGRAMS)) $(addsuffix .o, $(PROGRAMS))
- rm -f common.o $(WIN32RES)
+ rm -f common.o parallel.o $(WIN32RES)
rm -rf tmp_check
check:
diff --git a/src/bin/scripts/parallel.c b/src/bin/scripts/parallel.c
new file mode 100644
index 0000000000..5e4505e9fe
--- /dev/null
+++ b/src/bin/scripts/parallel.c
@@ -0,0 +1,282 @@
+/*-------------------------------------------------------------------------
+ *
+ * parallel.c
+ * Parallel support for bin/scripts/
+ *
+ *
+ * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/bin/scripts/parallel.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#ifdef HAVE_SYS_SELECT_H
+#include <sys/select.h>
+#endif
+
+#include "common.h"
+#include "common/logging.h"
+#include "parallel.h"
+
+#define ERRCODE_UNDEFINED_TABLE "42P01"
+
+
+/*
+ * GetIdleSlot
+ * Return a connection slot that is ready to execute a command.
+ *
+ * We return the first slot we find that is marked isFree, if one is;
+ * otherwise, we loop on select() until one socket becomes available. When
+ * this happens, we read the whole set and mark as free all sockets that become
+ * available.
+ *
+ * If an error occurs, NULL is returned.
+ */
+ParallelSlot *
+GetIdleSlot(ParallelSlot slots[], int numslots,
+ const char *progname)
+{
+ int i;
+ int firstFree = -1;
+
+ /* Any connection already known free? */
+ for (i = 0; i < numslots; i++)
+ {
+ if (slots[i].isFree)
+ return slots + i;
+ }
+
+ /*
+ * No free slot found, so wait until one of the connections has finished
+ * its task and return the available slot.
+ */
+ while (firstFree < 0)
+ {
+ fd_set slotset;
+ int maxFd = 0;
+ bool aborting;
+
+ /* We must reconstruct the fd_set for each call to select_loop */
+ FD_ZERO(&slotset);
+
+ for (i = 0; i < numslots; i++)
+ {
+ int sock = PQsocket(slots[i].connection);
+
+ /*
+ * We don't really expect any connections to lose their sockets
+ * after startup, but just in case, cope by ignoring them.
+ */
+ if (sock < 0)
+ continue;
+
+ FD_SET(sock, &slotset);
+ if (sock > maxFd)
+ maxFd = sock;
+ }
+
+ SetCancelConn(slots->connection);
+ i = select_loop(maxFd, &slotset, &aborting);
+ ResetCancelConn();
+
+ if (aborting)
+ {
+ /*
+ * We set the cancel-receiving connection to the one in the zeroth
+ * slot above, so fetch the error from there.
+ */
+ GetQueryResult(slots->connection, progname);
+ return NULL;
+ }
+ Assert(i != 0);
+
+ for (i = 0; i < numslots; i++)
+ {
+ int sock = PQsocket(slots[i].connection);
+
+ if (sock >= 0 && FD_ISSET(sock, &slotset))
+ {
+ /* select() says input is available, so consume it */
+ PQconsumeInput(slots[i].connection);
+ }
+
+ /* Collect result(s) as long as any are available */
+ while (!PQisBusy(slots[i].connection))
+ {
+ PGresult *result = PQgetResult(slots[i].connection);
+
+ if (result != NULL)
+ {
+ /* Check and discard the command result */
+ if (!ProcessQueryResult(slots[i].connection, result,
+ progname))
+ return NULL;
+ }
+ else
+ {
+ /* This connection has become idle */
+ slots[i].isFree = true;
+ if (firstFree < 0)
+ firstFree = i;
+ break;
+ }
+ }
+ }
+ }
+
+ return slots + firstFree;
+}
+
+/*
+ * ProcessQueryResult
+ *
+ * Process (and delete) a query result. Returns true if there's no error,
+ * false otherwise -- but errors about trying to vacuum a missing relation
+ * are reported and subsequently ignored.
+ */
+bool
+ProcessQueryResult(PGconn *conn, PGresult *result, const char *progname)
+{
+ /*
+ * If it's an error, report it. Errors about a missing table are harmless
+ * so we continue processing; but die for other errors.
+ */
+ if (PQresultStatus(result) != PGRES_COMMAND_OK)
+ {
+ char *sqlState = PQresultErrorField(result, PG_DIAG_SQLSTATE);
+
+ pg_log_error("processing of database \"%s\" failed: %s",
+ PQdb(conn), PQerrorMessage(conn));
+
+ if (sqlState && strcmp(sqlState, ERRCODE_UNDEFINED_TABLE) != 0)
+ {
+ PQclear(result);
+ return false;
+ }
+ }
+
+ PQclear(result);
+ return true;
+}
+
+/*
+ * GetQueryResult
+ *
+ * Pump the conn till it's dry of results; return false if any are errors.
+ * Note that this will block if the conn is busy.
+ */
+bool
+GetQueryResult(PGconn *conn, const char *progname)
+{
+ bool ok = true;
+ PGresult *result;
+
+ SetCancelConn(conn);
+ while ((result = PQgetResult(conn)) != NULL)
+ {
+ if (!ProcessQueryResult(conn, result, progname))
+ ok = false;
+ }
+ ResetCancelConn();
+ return ok;
+}
+
+/*
+ * DisconnectDatabase
+ * Disconnect the connection associated with the given slot
+ */
+void
+DisconnectDatabase(ParallelSlot *slot)
+{
+ char errbuf[256];
+
+ if (!slot->connection)
+ return;
+
+ if (PQtransactionStatus(slot->connection) == PQTRANS_ACTIVE)
+ {
+ PGcancel *cancel;
+
+ if ((cancel = PQgetCancel(slot->connection)))
+ {
+ (void) PQcancel(cancel, errbuf, sizeof(errbuf));
+ PQfreeCancel(cancel);
+ }
+ }
+
+ PQfinish(slot->connection);
+ slot->connection = NULL;
+}
+
+/*
+ * Loop on select() until a descriptor from the given set becomes readable.
+ *
+ * If we get a cancel request while we're waiting, we forego all further
+ * processing and set the *aborting flag to true. The return value must be
+ * ignored in this case. Otherwise, *aborting is set to false.
+ */
+int
+select_loop(int maxFd, fd_set *workerset, bool *aborting)
+{
+ int i;
+ fd_set saveSet = *workerset;
+
+ if (CancelRequested)
+ {
+ *aborting = true;
+ return -1;
+ }
+ else
+ *aborting = false;
+
+ for (;;)
+ {
+ /*
+ * On Windows, we need to check once in a while for cancel requests;
+ * on other platforms we rely on select() returning when interrupted.
+ */
+ struct timeval *tvp;
+#ifdef WIN32
+ struct timeval tv = {0, 1000000};
+
+ tvp = &tv;
+#else
+ tvp = NULL;
+#endif
+
+ *workerset = saveSet;
+ i = select(maxFd + 1, workerset, NULL, NULL, tvp);
+
+#ifdef WIN32
+ if (i == SOCKET_ERROR)
+ {
+ i = -1;
+
+ if (WSAGetLastError() == WSAEINTR)
+ errno = EINTR;
+ }
+#endif
+
+ if (i < 0 && errno == EINTR)
+ continue; /* ignore this */
+ if (i < 0 || CancelRequested)
+ *aborting = true; /* but not this */
+ if (i == 0)
+ continue; /* timeout (Win32 only) */
+ break;
+ }
+
+ return i;
+}
+
+void
+init_slot(ParallelSlot *slot, PGconn *conn)
+{
+ slot->connection = conn;
+ /* Initially assume connection is idle */
+ slot->isFree = true;
+}
diff --git a/src/bin/scripts/parallel.h b/src/bin/scripts/parallel.h
new file mode 100644
index 0000000000..be58e0bb96
--- /dev/null
+++ b/src/bin/scripts/parallel.h
@@ -0,0 +1,34 @@
+/*
+ * parallel.h
+ * Parallel support for bin/scripts/
+ *
+ * Copyright (c) 2003-2019, PostgreSQL Global Development Group
+ *
+ * src/bin/scripts/parallel.h
+ */
+#ifndef SCRIPTS_PARALLEL_H
+#define SCRIPTS_PARALLEL_H
+
+/* Parallel processing stuff */
+typedef struct ParallelSlot
+{
+ PGconn *connection; /* One connection */
+ bool isFree; /* Is it known to be idle? */
+} ParallelSlot;
+
+extern ParallelSlot *GetIdleSlot(ParallelSlot slots[], int numslots,
+ const char *progname);
+
+extern bool ProcessQueryResult(PGconn *conn, PGresult *result,
+ const char *progname);
+
+extern bool GetQueryResult(PGconn *conn, const char *progname);
+
+extern void DisconnectDatabase(ParallelSlot *slot);
+
+extern int select_loop(int maxFd, fd_set *workerset, bool *aborting);
+
+extern void init_slot(ParallelSlot *slot, PGconn *conn);
+
+
+#endif /* SCRIPTS_PARALLEL_H */
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index df2a315f95..80c9341a5b 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -12,10 +12,6 @@
#include "postgres_fe.h"
-#ifdef HAVE_SYS_SELECT_H
-#include <sys/select.h>
-#endif
-
#include "catalog/pg_class_d.h"
#include "common.h"
@@ -23,17 +19,9 @@
#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "parallel.h"
-#define ERRCODE_UNDEFINED_TABLE "42P01"
-
-/* Parallel vacuuming stuff */
-typedef struct ParallelSlot
-{
- PGconn *connection; /* One connection */
- bool isFree; /* Is it known to be idle? */
-} ParallelSlot;
-
/* vacuum options controlled by user flags */
typedef struct vacuumingOptions
{
@@ -71,20 +59,6 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
const char *table, const char *progname, bool async);
-static ParallelSlot *GetIdleSlot(ParallelSlot slots[], int numslots,
- const char *progname);
-
-static bool ProcessQueryResult(PGconn *conn, PGresult *result,
- const char *progname);
-
-static bool GetQueryResult(PGconn *conn, const char *progname);
-
-static void DisconnectDatabase(ParallelSlot *slot);
-
-static int select_loop(int maxFd, fd_set *workerset, bool *aborting);
-
-static void init_slot(ParallelSlot *slot, PGconn *conn);
-
static void help(const char *progname);
/* For analyze-in-stages mode */
@@ -953,262 +927,6 @@ run_vacuum_command(PGconn *conn, const char *sql, bool echo,
}
}
-/*
- * GetIdleSlot
- * Return a connection slot that is ready to execute a command.
- *
- * We return the first slot we find that is marked isFree, if one is;
- * otherwise, we loop on select() until one socket becomes available. When
- * this happens, we read the whole set and mark as free all sockets that become
- * available.
- *
- * If an error occurs, NULL is returned.
- */
-static ParallelSlot *
-GetIdleSlot(ParallelSlot slots[], int numslots,
- const char *progname)
-{
- int i;
- int firstFree = -1;
-
- /* Any connection already known free? */
- for (i = 0; i < numslots; i++)
- {
- if (slots[i].isFree)
- return slots + i;
- }
-
- /*
- * No free slot found, so wait until one of the connections has finished
- * its task and return the available slot.
- */
- while (firstFree < 0)
- {
- fd_set slotset;
- int maxFd = 0;
- bool aborting;
-
- /* We must reconstruct the fd_set for each call to select_loop */
- FD_ZERO(&slotset);
-
- for (i = 0; i < numslots; i++)
- {
- int sock = PQsocket(slots[i].connection);
-
- /*
- * We don't really expect any connections to lose their sockets
- * after startup, but just in case, cope by ignoring them.
- */
- if (sock < 0)
- continue;
-
- FD_SET(sock, &slotset);
- if (sock > maxFd)
- maxFd = sock;
- }
-
- SetCancelConn(slots->connection);
- i = select_loop(maxFd, &slotset, &aborting);
- ResetCancelConn();
-
- if (aborting)
- {
- /*
- * We set the cancel-receiving connection to the one in the zeroth
- * slot above, so fetch the error from there.
- */
- GetQueryResult(slots->connection, progname);
- return NULL;
- }
- Assert(i != 0);
-
- for (i = 0; i < numslots; i++)
- {
- int sock = PQsocket(slots[i].connection);
-
- if (sock >= 0 && FD_ISSET(sock, &slotset))
- {
- /* select() says input is available, so consume it */
- PQconsumeInput(slots[i].connection);
- }
-
- /* Collect result(s) as long as any are available */
- while (!PQisBusy(slots[i].connection))
- {
- PGresult *result = PQgetResult(slots[i].connection);
-
- if (result != NULL)
- {
- /* Check and discard the command result */
- if (!ProcessQueryResult(slots[i].connection, result,
- progname))
- return NULL;
- }
- else
- {
- /* This connection has become idle */
- slots[i].isFree = true;
- if (firstFree < 0)
- firstFree = i;
- break;
- }
- }
- }
- }
-
- return slots + firstFree;
-}
-
-/*
- * ProcessQueryResult
- *
- * Process (and delete) a query result. Returns true if there's no error,
- * false otherwise -- but errors about trying to vacuum a missing relation
- * are reported and subsequently ignored.
- */
-static bool
-ProcessQueryResult(PGconn *conn, PGresult *result, const char *progname)
-{
- /*
- * If it's an error, report it. Errors about a missing table are harmless
- * so we continue processing; but die for other errors.
- */
- if (PQresultStatus(result) != PGRES_COMMAND_OK)
- {
- char *sqlState = PQresultErrorField(result, PG_DIAG_SQLSTATE);
-
- pg_log_error("vacuuming of database \"%s\" failed: %s",
- PQdb(conn), PQerrorMessage(conn));
-
- if (sqlState && strcmp(sqlState, ERRCODE_UNDEFINED_TABLE) != 0)
- {
- PQclear(result);
- return false;
- }
- }
-
- PQclear(result);
- return true;
-}
-
-/*
- * GetQueryResult
- *
- * Pump the conn till it's dry of results; return false if any are errors.
- * Note that this will block if the conn is busy.
- */
-static bool
-GetQueryResult(PGconn *conn, const char *progname)
-{
- bool ok = true;
- PGresult *result;
-
- SetCancelConn(conn);
- while ((result = PQgetResult(conn)) != NULL)
- {
- if (!ProcessQueryResult(conn, result, progname))
- ok = false;
- }
- ResetCancelConn();
- return ok;
-}
-
-/*
- * DisconnectDatabase
- * Disconnect the connection associated with the given slot
- */
-static void
-DisconnectDatabase(ParallelSlot *slot)
-{
- char errbuf[256];
-
- if (!slot->connection)
- return;
-
- if (PQtransactionStatus(slot->connection) == PQTRANS_ACTIVE)
- {
- PGcancel *cancel;
-
- if ((cancel = PQgetCancel(slot->connection)))
- {
- (void) PQcancel(cancel, errbuf, sizeof(errbuf));
- PQfreeCancel(cancel);
- }
- }
-
- PQfinish(slot->connection);
- slot->connection = NULL;
-}
-
-/*
- * Loop on select() until a descriptor from the given set becomes readable.
- *
- * If we get a cancel request while we're waiting, we forego all further
- * processing and set the *aborting flag to true. The return value must be
- * ignored in this case. Otherwise, *aborting is set to false.
- */
-static int
-select_loop(int maxFd, fd_set *workerset, bool *aborting)
-{
- int i;
- fd_set saveSet = *workerset;
-
- if (CancelRequested)
- {
- *aborting = true;
- return -1;
- }
- else
- *aborting = false;
-
- for (;;)
- {
- /*
- * On Windows, we need to check once in a while for cancel requests;
- * on other platforms we rely on select() returning when interrupted.
- */
- struct timeval *tvp;
-#ifdef WIN32
- struct timeval tv = {0, 1000000};
-
- tvp = &tv;
-#else
- tvp = NULL;
-#endif
-
- *workerset = saveSet;
- i = select(maxFd + 1, workerset, NULL, NULL, tvp);
-
-#ifdef WIN32
- if (i == SOCKET_ERROR)
- {
- i = -1;
-
- if (WSAGetLastError() == WSAEINTR)
- errno = EINTR;
- }
-#endif
-
- if (i < 0 && errno == EINTR)
- continue; /* ignore this */
- if (i < 0 || CancelRequested)
- *aborting = true; /* but not this */
- if (i == 0)
- continue; /* timeout (Win32 only) */
- break;
- }
-
- return i;
-}
-
-static void
-init_slot(ParallelSlot *slot, PGconn *conn)
-{
- slot->connection = conn;
- /* Initially assume connection is idle */
- slot->isFree = true;
-}
-
static void
help(const char *progname)
{
--
2.20.1
0003-Add-parallel-processing-to-reindexdb.patchapplication/octet-stream; name=0003-Add-parallel-processing-to-reindexdb.patchDownload
From 894d0752b3d3248c27d37e6f02428f30b08d4eea Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 28 Jun 2019 13:21:58 +0200
Subject: [PATCH 3/4] Add parallel processing to reindexdb
---
src/bin/scripts/Makefile | 2 +-
src/bin/scripts/parallel.c | 52 ++-
src/bin/scripts/parallel.h | 5 +-
src/bin/scripts/reindexdb.c | 546 ++++++++++++++++++++++++++---
src/bin/scripts/t/090_reindexdb.pl | 12 +-
src/bin/scripts/vacuumdb.c | 2 +-
6 files changed, 558 insertions(+), 61 deletions(-)
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 6979d8f9ff..bf2e82c594 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -29,7 +29,7 @@ dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
vacuumdb: vacuumdb.o common.o parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+reindexdb: reindexdb.o common.o parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
install: all installdirs
diff --git a/src/bin/scripts/parallel.c b/src/bin/scripts/parallel.c
index 5e4505e9fe..bab49becaf 100644
--- a/src/bin/scripts/parallel.c
+++ b/src/bin/scripts/parallel.c
@@ -29,16 +29,21 @@
* GetIdleSlot
* Return a connection slot that is ready to execute a command.
*
- * We return the first slot we find that is marked isFree, if one is;
- * otherwise, we loop on select() until one socket becomes available. When
- * this happens, we read the whole set and mark as free all sockets that become
- * available.
+ * If pending_only is false, we return the first slot we find that is marked
+ * isFree, if one is; otherwise, we loop on select() until one socket becomes
+ * available. When this happens, we read the whole set and mark as free all
+ * sockets that become available.
+ * If pending_only is true, we filter out the slots that don't have any pending
+ * work to do, so we only return slots where slot is not NULL. This is useful
+ * if the caller pushed a list of items to process (provided as the root
+ * SimpleStringListCell of a SimpleStringList), to make sure that all pushed
+ * work is completed before exiting the program.
*
* If an error occurs, NULL is returned.
*/
ParallelSlot *
GetIdleSlot(ParallelSlot slots[], int numslots,
- const char *progname)
+ const char *progname, bool pending_only)
{
int i;
int firstFree = -1;
@@ -47,7 +52,23 @@ GetIdleSlot(ParallelSlot slots[], int numslots,
for (i = 0; i < numslots; i++)
{
if (slots[i].isFree)
- return slots + i;
+ {
+ /*
+ * If the caller didn't ask to filter the slots with only the one
+ * having pending items to process, we can return the first free
+ * slot we find.
+ */
+ if (!pending_only)
+ return slots + i;
+
+ /*
+ * If the caller asked to filter slots having pending items to
+ * process, check the current item and return the slot if it's not
+ * empty
+ */
+ if (slots[i].cell)
+ return slots + i;
+ }
}
/*
@@ -121,7 +142,23 @@ GetIdleSlot(ParallelSlot slots[], int numslots,
/* This connection has become idle */
slots[i].isFree = true;
if (firstFree < 0)
- firstFree = i;
+ {
+ /*
+ * If the caller didn't ask to filter the slots with
+ * only the one having pending items to process, we can
+ * mark as usable the first free slot we find as free.
+ */
+ if (!pending_only)
+ firstFree = i;
+ /*
+ * If the caller asked to filter slots having pending
+ * items to process, we need to check the current item
+ * and mark this slot as usable only if there's a
+ * pending item
+ */
+ else if (slots[i].cell)
+ firstFree = i;
+ }
break;
}
}
@@ -277,6 +314,7 @@ void
init_slot(ParallelSlot *slot, PGconn *conn)
{
slot->connection = conn;
+ slot->cell = NULL;
/* Initially assume connection is idle */
slot->isFree = true;
}
diff --git a/src/bin/scripts/parallel.h b/src/bin/scripts/parallel.h
index be58e0bb96..f3ce696472 100644
--- a/src/bin/scripts/parallel.h
+++ b/src/bin/scripts/parallel.h
@@ -9,15 +9,18 @@
#ifndef SCRIPTS_PARALLEL_H
#define SCRIPTS_PARALLEL_H
+#include "fe_utils/simple_list.h"
+
/* Parallel processing stuff */
typedef struct ParallelSlot
{
PGconn *connection; /* One connection */
+ SimpleStringListCell *cell; /* Next item to process if any */
bool isFree; /* Is it known to be idle? */
} ParallelSlot;
extern ParallelSlot *GetIdleSlot(ParallelSlot slots[], int numslots,
- const char *progname);
+ const char *progname, bool pending_only);
extern bool ProcessQueryResult(PGconn *conn, PGresult *result,
const char *progname);
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index 3528de21e0..f8a78a1b4a 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -12,8 +12,10 @@
#include "postgres_fe.h"
#include "common.h"
#include "common/logging.h"
+#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "parallel.h"
typedef enum ReindexType
{
@@ -25,16 +27,29 @@ typedef enum ReindexType
} ReindexType;
-static void reindex_one_database(const char *name, const char *dbname,
- ReindexType type, const char *host,
+static ReindexType get_parallel_object_list(PGconn *conn, ReindexType type,
+ SimpleStringList *user_list,
+ SimplePtrList * process_list,
+ const char *progname, bool echo);
+static void reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
const char *port, const char *username,
enum trivalue prompt_password, const char *progname,
- bool echo, bool verbose, bool concurrently);
+ bool echo, bool verbose, bool concurrently,
+ int concurrentCons);
static void reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo,
- bool quiet, bool verbose, bool concurrently);
+ bool quiet, bool verbose, bool concurrently,
+ int concurrentCons);
+static void run_reindex_command(PGconn *conn, ReindexType type,
+ const char *name, const char *progname, bool echo,
+ bool verbose, bool concurrently, bool async);
+static void slot_process_item(ParallelSlot *slot, int *pending_conn,
+ ReindexType process_type, const char *progname, bool echo,
+ bool verbose, bool concurrently, bool parallel);
+
static void help(const char *progname);
int
@@ -54,6 +69,7 @@ main(int argc, char *argv[])
{"system", no_argument, NULL, 's'},
{"table", required_argument, NULL, 't'},
{"index", required_argument, NULL, 'i'},
+ {"jobs", required_argument, NULL, 'j'},
{"verbose", no_argument, NULL, 'v'},
{"concurrently", no_argument, NULL, 1},
{"maintenance-db", required_argument, NULL, 2},
@@ -79,6 +95,9 @@ main(int argc, char *argv[])
SimpleStringList indexes = {NULL, NULL};
SimpleStringList tables = {NULL, NULL};
SimpleStringList schemas = {NULL, NULL};
+ int concurrentCons = 1;
+ int tbl_count = 0,
+ nsp_count = 0;
pg_logging_init(argv[0]);
progname = get_progname(argv[0]);
@@ -87,7 +106,7 @@ main(int argc, char *argv[])
handle_help_version_opts(argc, argv, "reindexdb", help);
/* process command-line options */
- while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:v", long_options, &optindex)) != -1)
+ while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:j:v", long_options, &optindex)) != -1)
{
switch (c)
{
@@ -114,6 +133,7 @@ main(int argc, char *argv[])
break;
case 'S':
simple_string_list_append(&schemas, optarg);
+ nsp_count++;
break;
case 'd':
dbname = pg_strdup(optarg);
@@ -126,10 +146,25 @@ main(int argc, char *argv[])
break;
case 't':
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
case 'i':
simple_string_list_append(&indexes, optarg);
break;
+ case 'j':
+ concurrentCons = atoi(optarg);
+ if (concurrentCons <= 0)
+ {
+ pg_log_error("number of parallel jobs must be at least 1");
+ exit(1);
+ }
+ if (concurrentCons > FD_SETSIZE - 1)
+ {
+ pg_log_error("too many parallel jobs requested (maximum: %d)",
+ FD_SETSIZE - 1);
+ exit(1);
+ }
+ break;
case 'v':
verbose = true;
break;
@@ -194,7 +229,8 @@ main(int argc, char *argv[])
}
reindex_all_databases(maintenance_db, host, port, username,
- prompt_password, progname, echo, quiet, verbose, concurrently);
+ prompt_password, progname, echo, quiet, verbose,
+ concurrently, 1);
}
else if (syscatalog)
{
@@ -214,6 +250,12 @@ main(int argc, char *argv[])
exit(1);
}
+ if (concurrentCons > 1)
+ {
+ pg_log_error("cannot use multiple jobs to reindex system catalogs");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -224,9 +266,9 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- reindex_one_database(NULL, dbname, REINDEX_SYSTEM, host,
+ reindex_one_database(dbname, REINDEX_SYSTEM, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, 1);
}
else
{
@@ -241,61 +283,57 @@ main(int argc, char *argv[])
}
if (schemas.head != NULL)
- {
- SimpleStringListCell *cell;
-
- for (cell = schemas.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_SCHEMA, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, nsp_count));
if (indexes.head != NULL)
- {
- SimpleStringListCell *cell;
- for (cell = indexes.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_INDEX, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
- if (tables.head != NULL)
- {
- SimpleStringListCell *cell;
+ /*
+ * The number of threads will be checked in the function, as it's
+ * depending on the number of underlying tables
+ */
+ reindex_one_database(dbname, REINDEX_INDEX, &indexes, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ concurrentCons);
- for (cell = tables.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_TABLE, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ if (tables.head != NULL)
+ reindex_one_database(dbname, REINDEX_TABLE, &tables, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, tbl_count));
/*
* reindex database only if neither index nor table nor schema is
* specified
*/
if (indexes.head == NULL && tables.head == NULL && schemas.head == NULL)
- reindex_one_database(NULL, dbname, REINDEX_DATABASE, host,
+ reindex_one_database(dbname, REINDEX_DATABASE, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, concurrentCons);
}
exit(0);
}
static void
-reindex_one_database(const char *name, const char *dbname, ReindexType type,
- const char *host, const char *port, const char *username,
+reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
+ const char *port, const char *username,
enum trivalue prompt_password, const char *progname, bool echo,
- bool verbose, bool concurrently)
+ bool verbose, bool concurrently, int concurrentCons)
{
- PQExpBufferData sql;
PGconn *conn;
+ bool parallel = concurrentCons > 1;
+ ReindexType process_type = type;
+ SimplePtrList process_list = {0, NULL, NULL};
+ SimplePtrListCell *cell;
+ ParallelSlot *slots;
+ int i,
+ pending_conn;
+ bool failed = false;
conn = connectDatabase(dbname, host, port, username, prompt_password,
progname, echo, false, false);
@@ -308,6 +346,173 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
exit(1);
}
+ if (!parallel)
+ {
+ if (user_list)
+ {
+ /*
+ * In non parallel mode, if the user provided a list, just use it
+ * as-is
+ */
+ simple_ptr_list_append(&process_list, user_list);
+ }
+ else
+ {
+ /*
+ * Otherwise, create a dummy list with an empty string, as user
+ * requires an element.
+ */
+ SimpleStringList *dummy = palloc0(sizeof(SimpleStringList));
+
+ simple_string_list_append(dummy, "");
+ simple_ptr_list_append(&process_list, dummy);
+ }
+ }
+ else
+ {
+ /*
+ * Database-wide parallel reindex requires special processing. If
+ * multiple jobs were asked, we have to reindex system catalogs first,
+ * as they can't be processed in parallel.
+ */
+ if (type == REINDEX_DATABASE)
+ {
+ run_reindex_command(conn, REINDEX_SYSTEM, NULL, progname, echo, verbose,
+ concurrently, false);
+ }
+
+ /* Get the list of objects to process */
+ process_type = get_parallel_object_list(conn, type, user_list,
+ &process_list, progname, echo);
+ }
+
+ /* Lower down the number of required connections if needed. */
+ concurrentCons = Min(concurrentCons, process_list.size);
+
+ /* If no object was found, we're done. */
+ if (concurrentCons <= 0)
+ return;
+ if (concurrentCons == 1)
+ parallel = false;
+
+ slots = (ParallelSlot *) pg_malloc(sizeof(ParallelSlot) * concurrentCons);
+ init_slot(slots, conn);
+ if (parallel)
+ {
+ for (i = 1; i < concurrentCons; i++)
+ {
+ conn = connectDatabase(dbname, host, port, username, prompt_password,
+ progname, echo, false, true);
+ init_slot(slots + i, conn);
+ }
+ }
+
+ pending_conn = 0;
+ cell = process_list.head;
+ do
+ {
+ ParallelSlot *free_slot = NULL;
+ SimpleStringList *cur = (SimpleStringList *) cell->val;
+
+ if (CancelRequested)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ /*
+ * Get the connection slot to use. If in parallel mode, here we wait
+ * for one connection to become available if none already is. In
+ * non-parallel mode we simply use the only slot we have, which we
+ * know to be free.
+ */
+ if (parallel)
+ {
+ /*
+ * Get a free slot, waiting until one becomes free if none
+ * currently is.
+ */
+ free_slot = GetIdleSlot(slots, concurrentCons, progname, false);
+ if (!free_slot)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ free_slot->isFree = false;
+ }
+ else
+ free_slot = slots;
+
+ /*
+ * If the idle slot found as done processing its object list, we can
+ * pop an item from the global processing list and affect it to this
+ * slot
+ */
+ if (free_slot->cell == NULL)
+ {
+ free_slot->cell = cur->head;
+ cell = cell->next;
+ pending_conn++;
+ }
+
+ /* Consume an item for this slot's list */
+ slot_process_item(free_slot, &pending_conn, process_type, progname,
+ echo, verbose, concurrently, parallel);
+ } while (cell != NULL);
+
+ /*
+ * We now have walked through all the global processing list. We still
+ * have to make sure that all slots are done processing their local lists.
+ */
+ while (pending_conn > 0)
+ {
+ ParallelSlot *free_slot = NULL;
+
+ free_slot = GetIdleSlot(slots, concurrentCons, progname, true);
+ if (!free_slot)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ slot_process_item(free_slot, &pending_conn, process_type, progname,
+ echo, verbose, concurrently, parallel);
+
+ }
+
+ if (parallel)
+ {
+ int j;
+
+ /* wait for all connections to finish */
+ for (j = 0; j < concurrentCons; j++)
+ {
+ if (!GetQueryResult((slots + j)->connection, progname))
+ {
+ failed = true;
+ goto finish;
+ }
+ }
+ }
+
+finish:
+ for (i = 0; i < concurrentCons; i++)
+ DisconnectDatabase(slots + i);
+ pfree(slots);
+
+ if (failed)
+ exit(1);
+}
+
+static void
+run_reindex_command(PGconn *conn, ReindexType type, const char *name,
+ const char *progname, bool echo, bool verbose,
+ bool concurrently, bool async)
+{
+ PQExpBufferData sql;
+ bool status;
+
/* build the REINDEX query */
initPQExpBuffer(&sql);
@@ -358,7 +563,17 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
/* finish the query */
appendPQExpBufferChar(&sql, ';');
- if (!executeMaintenanceCommand(conn, sql.data, echo))
+ if (async)
+ {
+ if (echo)
+ printf("%s\n", sql.data);
+
+ status = PQsendQuery(conn, sql.data) == 1;
+ }
+ else
+ status = executeMaintenanceCommand(conn, sql.data, echo);
+
+ if (!status)
{
switch (type)
{
@@ -383,20 +598,249 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
name, PQdb(conn), PQerrorMessage(conn));
break;
}
- PQfinish(conn);
- exit(1);
+ if (!async)
+ {
+ PQfinish(conn);
+ exit(1);
+ }
}
- PQfinish(conn);
termPQExpBuffer(&sql);
}
+/*
+ * Prepare the list of objects to process by querying the catalogs.
+ *
+ * This function will fill the given process_list list with SimpleStringList
+ * objects, filtered by the list of objects that the script was provided with
+ * if any.
+ * Each SimpleStringList describes objects that can be processed by
+ * multiple connections. This is required as multiple indexes belonging to the
+ * same table cannot be processed in parallel.
+ */
+static ReindexType
+get_parallel_object_list(PGconn *conn, ReindexType type,
+ SimpleStringList *user_list,
+ SimplePtrList * process_list, const char *progname,
+ bool echo)
+{
+ ReindexType process_type = type;
+ PQExpBufferData buf;
+ PQExpBufferData catalog_query;
+ int i;
+
+ Assert(process_list->size == 0);
+
+ if (type == REINDEX_DATABASE)
+ {
+ PGresult *res;
+ SimpleStringList *tables;
+ int ntups;
+
+ Assert(user_list == NULL);
+
+ process_type = REINDEX_TABLE;
+
+ initPQExpBuffer(&catalog_query);
+
+ /*
+ * This query is run using a safe search_path, so there's no need to
+ * fully qualify everything.
+ */
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE ns.nspname != 'pg_catalog'\n"
+ " AND c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " ORDER BY c.relpages DESC;");
+
+ res = executeQuery(conn, catalog_query.data, progname, echo);
+ termPQExpBuffer(&catalog_query);
+
+ /*
+ * If no rows are returned, there are no matching tables, so we are
+ * done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return process_type;
+ }
+
+ /* Build qualified identifiers for each table */
+ initPQExpBuffer(&buf);
+ for (i = 0; i < ntups; i++)
+ {
+ tables = pg_malloc0(sizeof(SimpleStringList));
+
+ appendPQExpBufferStr(&buf,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+
+ simple_string_list_append(tables, buf.data);
+ simple_ptr_list_append(process_list, tables);
+ resetPQExpBuffer(&buf);
+ }
+ termPQExpBuffer(&buf);
+ PQclear(res);
+ }
+ else if (type == REINDEX_INDEX)
+ {
+ SimpleStringList *indexes = NULL;
+ SimpleStringListCell *cell;
+ PGresult *res;
+ int ntups;
+ char *prev_rel = NULL;
+
+ Assert(user_list != NULL);
+
+ initPQExpBuffer(&catalog_query);
+
+ /*
+ * Since we execute the constructed query with the default search_path
+ * (which could be unsafe), everything in this query MUST be fully
+ * qualified. Note that if the user provided catalog indexes, those
+ * would be processed in parallel with other indexes, with all the
+ * locking issues that would be implied.
+ */
+ appendPQExpBuffer(&catalog_query,
+ "SELECT ic.relname, ns.nspname, tc.oid\n"
+ " FROM pg_catalog.pg_index i\n"
+ " JOIN pg_catalog.pg_class ic"
+ " ON i.indexrelid OPERATOR(pg_catalog.=) ic.oid\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON ic.relnamespace OPERATOR(pg_catalog.=) ns.oid\n"
+ " JOIN pg_catalog.pg_class tc"
+ " ON i.indrelid OPERATOR(pg_catalog.=) tc.oid\n"
+ " WHERE i.indexrelid OPERATOR(pg_catalog.=) ANY (array[\n");
+
+ for (cell = user_list->head; cell; cell = cell->next)
+ {
+ if (cell != user_list->head)
+ appendPQExpBuffer(&catalog_query, ", ");
+
+ appendStringLiteralConn(&catalog_query, cell->val, conn);
+ appendPQExpBuffer(&catalog_query, "::pg_catalog.regclass");
+ }
+
+ /*
+ * We try to get the biggest indexes first so they're processed
+ * earlier. We require that all indexes belonging to the same table
+ * are contiguous, so we can only order by the underlying table size.
+ * We also need to order by the underlying table oid to make sure that
+ * indexes belonging to different tables of the same size are still
+ * correctly ordered.
+ */
+ appendPQExpBuffer(&catalog_query,
+ "\n])\n"
+ " ORDER BY tc.relpages DESC, tc.oid;");
+
+ executeCommand(conn, "RESET search_path;", progname, echo);
+ res = executeQuery(conn, catalog_query.data, progname, echo);
+ termPQExpBuffer(&catalog_query);
+ PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL,
+ progname, echo));
+
+ /*
+ * If no rows are returned, there are no matching tables, so we are
+ * done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return process_type;
+ }
+
+ indexes = pg_malloc0(sizeof(SimpleStringList));
+
+ /* Build a list of qualified index name, aggregated per table */
+ for (i = 0; i < ntups; i++)
+ {
+ if (!prev_rel)
+ prev_rel = pg_strdup(PQgetvalue(res, i, 2));
+
+ if (strcmp(PQgetvalue(res, i, 2), prev_rel) != 0)
+ {
+ simple_ptr_list_append(process_list, indexes);
+
+ indexes = pg_malloc0(sizeof(SimpleStringList));
+ pg_free(prev_rel);
+ prev_rel = pg_strdup(PQgetvalue(res, i, 2));
+ }
+
+ simple_string_list_append(indexes,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+ }
+ simple_ptr_list_append(process_list, indexes);
+
+ PQclear(res);
+ }
+
+ /*
+ * Any other object list is safe to dispatch a-is. Note that if the user
+ * provided catalog tables, those would be processed in parallel with
+ * other tables, with all the locking issues that would be implied.
+ */
+ else
+ {
+ SimpleStringList *res;
+ SimpleStringListCell *cell;
+
+ Assert(user_list != NULL);
+
+ for (cell = user_list->head; cell; cell = cell->next)
+ {
+ res = pg_malloc0(sizeof(SimpleStringList));
+
+ simple_string_list_append(res, cell->val);
+ simple_ptr_list_append(process_list, res);
+ }
+ }
+
+ return process_type;
+}
+
+static void
+slot_process_item(ParallelSlot *slot, int *pending_conn,
+ ReindexType process_type, const char *progname, bool echo,
+ bool verbose, bool concurrently, bool parallel)
+{
+ Assert(slot->cell);
+ Assert(*pending_conn > 0);
+
+ /*
+ * Process one item of current slot local list and advance it. If not in
+ * parallel mode, this terminates the program in case of an error. (The
+ * parallel case handles query errors in ProcessQueryResult through
+ * GetIdleSlot.)
+ */
+ run_reindex_command(slot->connection, process_type,
+ slot->cell->val,
+ progname, echo, verbose, concurrently,
+ parallel);
+
+ slot->cell = slot->cell->next;
+
+ /* Check if that was the last item of this list */
+ if (!slot->cell)
+ (*pending_conn)--;
+}
+
static void
reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo, bool quiet, bool verbose,
- bool concurrently)
+ bool concurrently, int concurrentCons)
{
PGconn *conn;
PGresult *result;
@@ -423,9 +867,10 @@ reindex_all_databases(const char *maintenance_db,
appendPQExpBuffer(&connstr, "dbname=");
appendConnStrVal(&connstr, dbname);
- reindex_one_database(NULL, connstr.data, REINDEX_DATABASE, host,
+ reindex_one_database(connstr.data, REINDEX_DATABASE, NULL, host,
port, username, prompt_password,
- progname, echo, verbose, concurrently);
+ progname, echo, verbose, concurrently,
+ concurrentCons);
}
termPQExpBuffer(&connstr);
@@ -444,6 +889,7 @@ help(const char *progname)
printf(_(" -d, --dbname=DBNAME database to reindex\n"));
printf(_(" -e, --echo show the commands being sent to the server\n"));
printf(_(" -i, --index=INDEX recreate specific index(es) only\n"));
+ printf(_(" -j, --jobs=NUM use this many concurrent connections to reindex\n"));
printf(_(" -q, --quiet don't write any messages\n"));
printf(_(" -s, --system reindex system catalogs\n"));
printf(_(" -S, --schema=SCHEMA reindex specific schema(s) only\n"));
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index 1af8ab70ad..a14853b3fc 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -3,7 +3,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 34;
+use Test::More tests => 39;
program_help_ok('reindexdb');
program_version_ok('reindexdb');
@@ -77,3 +77,13 @@ $node->command_ok(
$node->command_ok(
[qw(reindexdb --echo --system dbname=template1)],
'reindexdb system with connection string');
+
+# parallel processing
+$node->command_fails([qw(vreindexdb -j2 -s)],
+ 'vreindexdb cannot process systam catalogs in parallel');
+$node->issues_sql_like([qw(vreindexdb -j2)],
+ qr/statement: REINDEX SYSTEM postgres/,
+ 'Global and parallel reindex will issue a REINDEX SYSTEM');
+$node->issues_sql_like([qw(vreindexdb -j2)],
+ qr/statement: REINDEX TABLE public.test1/,
+ 'Global and parallel reindex will issue per-table REINDEX');
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 80c9341a5b..ad4ba0915c 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -652,7 +652,7 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
* Get a free slot, waiting until one becomes free if none
* currently is.
*/
- free_slot = GetIdleSlot(slots, concurrentCons, progname);
+ free_slot = GetIdleSlot(slots, concurrentCons, progname, false);
if (!free_slot)
{
failed = true;
--
2.20.1
0004-Add-a-glibc-dependent-option.patchapplication/octet-stream; name=0004-Add-a-glibc-dependent-option.patchDownload
From 25c668725ad0619351d32920ae882e9f9aec1e56 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 28 Jun 2019 14:21:17 +0200
Subject: [PATCH 4/4] Add a --glibc-dependent option.
When this flag is passed, all indexes that depend on a glibc collation will be
reindexed. This is especially important given the upcoming glibc 2.28 version
which break the ordering of most collations, including en_US. This option is
compatible with the --job, --dbname and the --all options (among others) but
not the --schema, --table and --index options.
---
src/bin/scripts/reindexdb.c | 387 ++++++++++++++++++-----
src/bin/scripts/t/092_reindexdb_glibc.pl | 106 +++++++
2 files changed, 412 insertions(+), 81 deletions(-)
create mode 100644 src/bin/scripts/t/092_reindexdb_glibc.pl
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index f8a78a1b4a..3742cebef0 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -10,6 +10,9 @@
*/
#include "postgres_fe.h"
+
+#include "catalog/pg_class_d.h"
+
#include "common.h"
#include "common/logging.h"
#include "fe_utils/connect.h"
@@ -30,25 +33,29 @@ typedef enum ReindexType
static ReindexType get_parallel_object_list(PGconn *conn, ReindexType type,
SimpleStringList *user_list,
SimplePtrList * process_list,
- const char *progname, bool echo);
+ const char *progname, bool echo,
+ bool glibc_dependent);
static void reindex_one_database(const char *dbname, ReindexType type,
SimpleStringList *user_list, const char *host,
const char *port, const char *username,
enum trivalue prompt_password, const char *progname,
bool echo, bool verbose, bool concurrently,
- int concurrentCons);
+ int concurrentCons, bool glibc_dependent);
static void reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo,
bool quiet, bool verbose, bool concurrently,
- int concurrentCons);
+ int concurrentCons, bool glibc_dependent);
static void run_reindex_command(PGconn *conn, ReindexType type,
const char *name, const char *progname, bool echo,
bool verbose, bool concurrently, bool async);
static void slot_process_item(ParallelSlot *slot, int *pending_conn,
ReindexType process_type, const char *progname, bool echo,
bool verbose, bool concurrently, bool parallel);
+static char *get_glibc_query(PGconn *conn);
+static void append_indexes_per_table(PGresult *res,
+ SimplePtrList *process_list);
static void help(const char *progname);
@@ -73,6 +80,7 @@ main(int argc, char *argv[])
{"verbose", no_argument, NULL, 'v'},
{"concurrently", no_argument, NULL, 1},
{"maintenance-db", required_argument, NULL, 2},
+ {"glibc-dependent", no_argument, NULL, 3},
{NULL, 0, NULL, 0}
};
@@ -92,6 +100,7 @@ main(int argc, char *argv[])
bool quiet = false;
bool verbose = false;
bool concurrently = false;
+ bool glibc_dependent = false;
SimpleStringList indexes = {NULL, NULL};
SimpleStringList tables = {NULL, NULL};
SimpleStringList schemas = {NULL, NULL};
@@ -174,6 +183,9 @@ main(int argc, char *argv[])
case 2:
maintenance_db = pg_strdup(optarg);
break;
+ case 3:
+ glibc_dependent = true;
+ break;
default:
fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname);
exit(1);
@@ -230,7 +242,7 @@ main(int argc, char *argv[])
reindex_all_databases(maintenance_db, host, port, username,
prompt_password, progname, echo, quiet, verbose,
- concurrently, 1);
+ concurrently, 1, glibc_dependent);
}
else if (syscatalog)
{
@@ -256,6 +268,12 @@ main(int argc, char *argv[])
exit(1);
}
+ if (glibc_dependent)
+ {
+ pg_log_error("cannot reindex glibc dependent objects and system catalogs at the same time");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -268,10 +286,17 @@ main(int argc, char *argv[])
reindex_one_database(dbname, REINDEX_SYSTEM, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently, 1);
+ echo, verbose, concurrently, 1, false);
}
else
{
+ if (glibc_dependent && (schemas.head != NULL || indexes.head != NULL
+ || tables.head != NULL))
+ {
+ pg_log_error("cannot reindex glibc dependent objects and a subset of objects");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -286,7 +311,7 @@ main(int argc, char *argv[])
reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host,
port, username, prompt_password, progname,
echo, verbose, concurrently,
- Min(concurrentCons, nsp_count));
+ Min(concurrentCons, nsp_count), false);
if (indexes.head != NULL)
@@ -297,13 +322,13 @@ main(int argc, char *argv[])
reindex_one_database(dbname, REINDEX_INDEX, &indexes, host,
port, username, prompt_password, progname,
echo, verbose, concurrently,
- concurrentCons);
+ concurrentCons, false);
if (tables.head != NULL)
reindex_one_database(dbname, REINDEX_TABLE, &tables, host,
port, username, prompt_password, progname,
echo, verbose, concurrently,
- Min(concurrentCons, tbl_count));
+ Min(concurrentCons, tbl_count), false);
/*
* reindex database only if neither index nor table nor schema is
@@ -312,7 +337,8 @@ main(int argc, char *argv[])
if (indexes.head == NULL && tables.head == NULL && schemas.head == NULL)
reindex_one_database(dbname, REINDEX_DATABASE, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently, concurrentCons);
+ echo, verbose, concurrently, concurrentCons,
+ glibc_dependent);
}
exit(0);
@@ -323,7 +349,8 @@ reindex_one_database(const char *dbname, ReindexType type,
SimpleStringList *user_list, const char *host,
const char *port, const char *username,
enum trivalue prompt_password, const char *progname, bool echo,
- bool verbose, bool concurrently, int concurrentCons)
+ bool verbose, bool concurrently, int concurrentCons,
+ bool glibc_dependent)
{
PGconn *conn;
bool parallel = concurrentCons > 1;
@@ -350,12 +377,58 @@ reindex_one_database(const char *dbname, ReindexType type,
{
if (user_list)
{
+ Assert(!glibc_dependent);
+
/*
* In non parallel mode, if the user provided a list, just use it
* as-is
*/
simple_ptr_list_append(&process_list, user_list);
}
+ else if (glibc_dependent)
+ {
+ SimpleStringList *dummy = palloc0(sizeof(SimpleStringList));
+ PGresult *res;
+ PQExpBufferData buf;
+ int ntups;
+ int i;
+
+ process_type = REINDEX_INDEX;
+
+ run_reindex_command(conn, REINDEX_SYSTEM, NULL, progname, echo,
+ verbose, concurrently, glibc_dependent);
+
+ res = executeQuery(conn, get_glibc_query(conn), progname, echo);
+
+ /*
+ * If no rows are returned, there are no matching tables, so we
+ * are done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return;
+ }
+
+ /* Build qualified identifiers for each table */
+ initPQExpBuffer(&buf);
+ for (i = 0; i < ntups; i++)
+ {
+ appendPQExpBufferStr(&buf,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+
+ simple_string_list_append(dummy, buf.data);
+
+ resetPQExpBuffer(&buf);
+ }
+ simple_ptr_list_append(&process_list, dummy);
+
+ termPQExpBuffer(&buf);
+ PQclear(res);
+ }
else
{
/*
@@ -383,7 +456,8 @@ reindex_one_database(const char *dbname, ReindexType type,
/* Get the list of objects to process */
process_type = get_parallel_object_list(conn, type, user_list,
- &process_list, progname, echo);
+ &process_list, progname, echo,
+ glibc_dependent);
}
/* Lower down the number of required connections if needed. */
@@ -622,7 +696,7 @@ static ReindexType
get_parallel_object_list(PGconn *conn, ReindexType type,
SimpleStringList *user_list,
SimplePtrList * process_list, const char *progname,
- bool echo)
+ bool echo, bool glibc_dependent)
{
ReindexType process_type = type;
PQExpBufferData buf;
@@ -639,66 +713,88 @@ get_parallel_object_list(PGconn *conn, ReindexType type,
Assert(user_list == NULL);
- process_type = REINDEX_TABLE;
+ if (glibc_dependent)
+ {
+ process_type = REINDEX_INDEX;
- initPQExpBuffer(&catalog_query);
+ res = executeQuery(conn, get_glibc_query(conn), progname, echo);
- /*
- * This query is run using a safe search_path, so there's no need to
- * fully qualify everything.
- */
- appendPQExpBuffer(&catalog_query,
- "SELECT c.relname, ns.nspname\n"
- " FROM pg_catalog.pg_class c\n"
- " JOIN pg_catalog.pg_namespace ns"
- " ON c.relnamespace = ns.oid\n"
- " WHERE ns.nspname != 'pg_catalog'\n"
- " AND c.relkind IN ("
- CppAsString2(RELKIND_RELATION) ", "
- CppAsString2(RELKIND_MATVIEW) ")\n"
- " ORDER BY c.relpages DESC;");
-
- res = executeQuery(conn, catalog_query.data, progname, echo);
- termPQExpBuffer(&catalog_query);
+ /*
+ * If no rows are returned, there are no matching tables, so we
+ * are done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return process_type;
+ }
- /*
- * If no rows are returned, there are no matching tables, so we are
- * done.
- */
- ntups = PQntuples(res);
- if (ntups == 0)
- {
- PQclear(res);
- PQfinish(conn);
- return process_type;
+ append_indexes_per_table(res, process_list);
}
-
- /* Build qualified identifiers for each table */
- initPQExpBuffer(&buf);
- for (i = 0; i < ntups; i++)
+ else
{
- tables = pg_malloc0(sizeof(SimpleStringList));
+ process_type = REINDEX_TABLE;
+
+ initPQExpBuffer(&catalog_query);
+
+ /*
+ * This query is run using a safe search_path, so there's no need
+ * to fully qualify everything.
+ */
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE ns.nspname != 'pg_catalog'\n"
+ " AND c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " ORDER BY c.relpages DESC;");
+
+ res = executeQuery(conn, catalog_query.data, progname, echo);
+ termPQExpBuffer(&catalog_query);
+
+ /*
+ * If no rows are returned, there are no matching tables, so we are
+ * done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return process_type;
+ }
+
+ /* Build qualified identifiers for each table */
+ initPQExpBuffer(&buf);
+ for (i = 0; i < ntups; i++)
+ {
+ tables = pg_malloc0(sizeof(SimpleStringList));
- appendPQExpBufferStr(&buf,
- fmtQualifiedId(PQgetvalue(res, i, 1),
- PQgetvalue(res, i, 0)));
+ appendPQExpBufferStr(&buf,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
- simple_string_list_append(tables, buf.data);
- simple_ptr_list_append(process_list, tables);
- resetPQExpBuffer(&buf);
+ simple_string_list_append(tables, buf.data);
+ simple_ptr_list_append(process_list, tables);
+ resetPQExpBuffer(&buf);
+ }
+ termPQExpBuffer(&buf);
+ PQclear(res);
}
- termPQExpBuffer(&buf);
- PQclear(res);
}
else if (type == REINDEX_INDEX)
{
- SimpleStringList *indexes = NULL;
SimpleStringListCell *cell;
PGresult *res;
int ntups;
- char *prev_rel = NULL;
Assert(user_list != NULL);
+ Assert(!glibc_dependent);
initPQExpBuffer(&catalog_query);
@@ -710,7 +806,8 @@ get_parallel_object_list(PGconn *conn, ReindexType type,
* locking issues that would be implied.
*/
appendPQExpBuffer(&catalog_query,
- "SELECT ic.relname, ns.nspname, tc.oid\n"
+ "SELECT ic.relname AS index_relname,"
+ " ns.nspname AS index_nspname, tc.oid AS table_oid\n"
" FROM pg_catalog.pg_index i\n"
" JOIN pg_catalog.pg_class ic"
" ON i.indexrelid OPERATOR(pg_catalog.=) ic.oid\n"
@@ -759,28 +856,7 @@ get_parallel_object_list(PGconn *conn, ReindexType type,
return process_type;
}
- indexes = pg_malloc0(sizeof(SimpleStringList));
-
- /* Build a list of qualified index name, aggregated per table */
- for (i = 0; i < ntups; i++)
- {
- if (!prev_rel)
- prev_rel = pg_strdup(PQgetvalue(res, i, 2));
-
- if (strcmp(PQgetvalue(res, i, 2), prev_rel) != 0)
- {
- simple_ptr_list_append(process_list, indexes);
-
- indexes = pg_malloc0(sizeof(SimpleStringList));
- pg_free(prev_rel);
- prev_rel = pg_strdup(PQgetvalue(res, i, 2));
- }
-
- simple_string_list_append(indexes,
- fmtQualifiedId(PQgetvalue(res, i, 1),
- PQgetvalue(res, i, 0)));
- }
- simple_ptr_list_append(process_list, indexes);
+ append_indexes_per_table(res, process_list);
PQclear(res);
}
@@ -796,6 +872,7 @@ get_parallel_object_list(PGconn *conn, ReindexType type,
SimpleStringListCell *cell;
Assert(user_list != NULL);
+ Assert(!glibc_dependent);
for (cell = user_list->head; cell; cell = cell->next)
{
@@ -809,6 +886,152 @@ get_parallel_object_list(PGconn *conn, ReindexType type,
return process_type;
}
+/*
+ * This function returns a query to retrieve all indexes that depends on a
+ * glibc stable sort ordering, and thus could get corrupted if that order
+ * changed. The returned query will be executed using a safe search_path, so
+ * there's no need to fully qualify everything.
+ *
+ * Indexes on the system catalogs are excluded, as those would get reindexed
+ * explicitly if a reindex on glibc-dependent indexes is asked.
+ *
+ * The list of impacted indexes is retrieved using the indclass field in
+ * pg_index and not the indkey field, as indexes must be retrieved too. The
+ * underlying type is then joined, and only indexes containing at least one
+ * column of type category String are kept. We however ignore special opclass
+ * varchar_pattern_ops and text_pattern_ops, as those do not depend on the
+ * underlying collation order.
+ *
+ * Note that there's no knowledge about specific glibc version or collation
+ * name.
+ */
+static char *
+get_glibc_query(PGconn *conn)
+{
+ /* pg 10 added pg_collation */
+ if (PQserverVersion(conn) >= 100000)
+ return "SELECT DISTINCT ic.relname AS index_relname,"
+ " n.nspname AS index_nspname, tc.oid AS table_oid,"
+ " pg_catalog.pg_indexes_size(tc.oid)"
+ " FROM pg_catalog.pg_class AS tc\n"
+ " JOIN pg_catalog.pg_index AS ix ON ix.indrelid = tc.oid\n"
+ " JOIN pg_catalog.pg_namespace AS n ON n.oid = tc.relnamespace\n"
+ " JOIN pg_catalog.pg_class AS ic ON ic.oid = ix.indexrelid\n"
+ " JOIN pg_catalog.pg_opclass AS opc ON opc.oid = ANY(ix.indclass)\n"
+ " JOIN pg_catalog.pg_type AS t ON t.oid = opc.opcintype\n"
+ " JOIN pg_catalog.pg_collation AS coll ON coll.oid = t.typcollation\n"
+ " WHERE t.typcategory = 'S'\n"
+ " AND n.nspname != 'pg_catalog'\n"
+ " AND tc.relkind IN ('m', 'r')\n"
+ " AND opc.opcname NOT IN ('text_pattern_ops', 'varchar_pattern_ops')\n"
+ " AND coll.collprovider IN ('d', 'c')\n"
+ " ORDER BY pg_catalog.pg_indexes_size(tc.oid) DESC;";
+ /* pg 9.3 added materialized views */
+ else if (PQserverVersion(conn) >= 90300)
+ return "SELECT DISTINCT ic.relname AS index_relname,"
+ " n.nspname AS index_nspname, tc.oid AS table_oid,"
+ " pg_catalog.pg_indexes_size(tc.oid)"
+ " FROM pg_catalog.pg_class AS tc\n"
+ " JOIN pg_catalog.pg_index AS ix ON ix.indrelid = tc.oid\n"
+ " JOIN pg_catalog.pg_namespace AS n ON n.oid = tc.relnamespace\n"
+ " JOIN pg_catalog.pg_class AS ic ON ic.oid = ix.indexrelid\n"
+ " JOIN pg_catalog.pg_opclass AS opc ON opc.oid = ANY(ix.indclass)\n"
+ " JOIN pg_catalog.pg_type AS t ON t.oid = opc.opcintype\n"
+ " WHERE t.typcategory = 'S'\n"
+ " AND n.nspname != 'pg_catalog'\n"
+ " AND tc.relkind IN ('m', 'r')\n"
+ " AND opc.opcname NOT IN ('text_pattern_ops', 'varchar_pattern_ops')\n"
+ " ORDER BY pg_catalog.pg_indexes_size(tc.oid) DESC;";
+ /* pg 9.0 added pg_indexes_size() */
+ else if (PQserverVersion(conn) >= 90000)
+ return "SELECT DISTINCT ic.relname AS index_relname,"
+ " n.nspname AS index_nspname, tc.oid AS table_oid,"
+ " pg_catalog.pg_indexes_size(tc.oid)"
+ " FROM pg_catalog.pg_class AS tc\n"
+ " JOIN pg_catalog.pg_index AS ix ON ix.indrelid = tc.oid\n"
+ " JOIN pg_catalog.pg_namespace AS n ON n.oid = tc.relnamespace\n"
+ " JOIN pg_catalog.pg_class AS ic ON ic.oid = ix.indexrelid\n"
+ " JOIN pg_catalog.pg_opclass AS opc ON opc.oid = ANY(ix.indclass)\n"
+ " JOIN pg_catalog.pg_type AS t ON t.oid = opc.opcintype\n"
+ " WHERE t.typcategory = 'S'\n"
+ " AND n.nspname != 'pg_catalog'\n"
+ " AND tc.relkind = 'r'\n"
+ " AND opc.opcname NOT IN ('text_pattern_ops', 'varchar_pattern_ops')\n"
+ " ORDER BY pg_catalog.pg_indexes_size(tc.oid) DESC;";
+ /* pg 8.1 added pg_relation_size() */
+ else if (PQserverVersion(conn) >= 80100)
+ return "SELECT DISTINCT ic.relname AS index_relname,"
+ " n.nspname AS index_nspname, tc.oid AS table_oid,"
+ " pg_catalog.pg_relation_size(tc.oid)"
+ " FROM pg_catalog.pg_class AS tc\n"
+ " JOIN pg_catalog.pg_index AS ix ON ix.indrelid = tc.oid\n"
+ " JOIN pg_catalog.pg_namespace AS n ON n.oid = tc.relnamespace\n"
+ " JOIN pg_catalog.pg_class AS ic ON ic.oid = ix.indexrelid\n"
+ " JOIN pg_catalog.pg_opclass AS opc ON opc.oid = ANY(ix.indclass)\n"
+ " JOIN pg_catalog.pg_type AS t ON t.oid = opc.opcintype\n"
+ " WHERE t.typcategory = 'S'\n"
+ " AND n.nspname != 'pg_catalog'\n"
+ " AND tc.relkind = 'r'\n"
+ " AND opc.opcname NOT IN ('text_pattern_ops', 'varchar_pattern_ops')\n"
+ " ORDER BY pg_catalog.pg_relation_size(tc.oid) DESC;";
+ else
+ return "SELECT DISTINCT ic.relname AS index_relname,"
+ " n.nspname AS index_nspname, tc.oid AS table_oid,"
+ " FROM pg_catalog.pg_class AS tc\n"
+ " JOIN pg_catalog.pg_index AS ix ON ix.indrelid = tc.oid\n"
+ " JOIN pg_catalog.pg_namespace AS n ON n.oid = tc.relnamespace\n"
+ " JOIN pg_catalog.pg_class AS ic ON ic.oid = ix.indexrelid\n"
+ " JOIN pg_catalog.pg_opclass AS opc ON opc.oid = ANY(ix.indclass)\n"
+ " JOIN pg_catalog.pg_type AS t ON t.oid = opc.opcintype\n"
+ " WHERE t.typcategory = 'S'\n"
+ " AND n.nspname != 'pg_catalog'\n"
+ " AND tc.relkind = 'r'\n"
+ " AND opc.opcname NOT IN ('text_pattern_ops', 'varchar_pattern_ops');";
+}
+
+/*
+ * Build a list of qualified index name, per table.
+ *
+ * The result set must be:
+ * (index_relname, index_nspname, table_oid)
+ */
+static void
+append_indexes_per_table(PGresult *res, SimplePtrList *process_list)
+{
+ SimpleStringList *indexes = NULL;
+ char *prev_rel = NULL;
+ int ntups = PQntuples(res);
+ int i;
+
+ indexes = pg_malloc0(sizeof(SimpleStringList));
+
+ Assert(strcmp(PQfname(res, 0), "index_relname") == 0);
+ Assert(strcmp(PQfname(res, 1), "index_nspname") == 0);
+ Assert(strcmp(PQfname(res, 2), "table_oid") == 0);
+
+ /* Build a list of qualified index name, per table */
+ for (i = 0; i < ntups; i++)
+ {
+ if (!prev_rel)
+ prev_rel = pg_strdup(PQgetvalue(res, i, 2));
+
+ if (strcmp(PQgetvalue(res, i, 2), prev_rel) != 0)
+ {
+ simple_ptr_list_append(process_list, indexes);
+
+ indexes = pg_malloc0(sizeof(SimpleStringList));
+ pg_free(prev_rel);
+ prev_rel = pg_strdup(PQgetvalue(res, i, 2));
+ }
+
+ simple_string_list_append(indexes,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+ }
+
+ simple_ptr_list_append(process_list, indexes);
+}
+
static void
slot_process_item(ParallelSlot *slot, int *pending_conn,
ReindexType process_type, const char *progname, bool echo,
@@ -840,7 +1063,8 @@ reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo, bool quiet, bool verbose,
- bool concurrently, int concurrentCons)
+ bool concurrently, int concurrentCons,
+ bool glibc_dependent)
{
PGconn *conn;
PGresult *result;
@@ -870,7 +1094,7 @@ reindex_all_databases(const char *maintenance_db,
reindex_one_database(connstr.data, REINDEX_DATABASE, NULL, host,
port, username, prompt_password,
progname, echo, verbose, concurrently,
- concurrentCons);
+ concurrentCons, glibc_dependent);
}
termPQExpBuffer(&connstr);
@@ -888,6 +1112,7 @@ help(const char *progname)
printf(_(" --concurrently reindex concurrently\n"));
printf(_(" -d, --dbname=DBNAME database to reindex\n"));
printf(_(" -e, --echo show the commands being sent to the server\n"));
+ printf(_(" --glibc-dependent only reindex indexes depending on GLIBC\n"));
printf(_(" -i, --index=INDEX recreate specific index(es) only\n"));
printf(_(" -j, --jobs=NUM use this many concurrent connections to reindex\n"));
printf(_(" -q, --quiet don't write any messages\n"));
diff --git a/src/bin/scripts/t/092_reindexdb_glibc.pl b/src/bin/scripts/t/092_reindexdb_glibc.pl
new file mode 100644
index 0000000000..22b07d2acb
--- /dev/null
+++ b/src/bin/scripts/t/092_reindexdb_glibc.pl
@@ -0,0 +1,106 @@
+use strict;
+use warnings;
+
+use PostgresNode;
+use TestLib;
+use Test::More tests => 60;
+
+if (exists $ENV{MY_PG_REGRESS})
+{
+ $ENV{PG_REGRESS} = $ENV{MY_PG_REGRESS};
+}
+
+my $node = get_new_node('main');
+$node->init;
+$node->start;
+
+$ENV{PGOPTIONS} = '--client-min-messages=WARNING';
+
+# glibc-dependent processing
+my %idxs;
+my %newidxs;
+
+$node->psql('postgres', 'CREATE TABLE "TEST"(id integer, val text)');
+$node->psql('postgres', 'CREATE INDEX idx1 ON "TEST"(id)');
+$node->psql('postgres', 'CREATE INDEX "IDX2" ON "TEST"(val)');
+$node->psql('postgres', 'CREATE INDEX idx3 ON "TEST"(val text_pattern_ops)');
+$node->psql('postgres', 'CREATE INDEX idx4 ON "TEST"(val text_pattern_ops, val)');
+$node->psql('postgres', 'CREATE INDEX idx5 ON "TEST"((lower(val)))');
+$node->psql('postgres', 'CREATE INDEX idx6 ON "TEST"((lower(val)) text_pattern_ops)');
+$node->psql('postgres', 'CREATE INDEX idx7 ON "TEST"((lower(val)), (lower(val)) text_pattern_ops)');
+
+# get original indexes' relfilenode
+foreach my $idx ('idx1', 'IDX2', 'idx3', 'idx4', 'idx5', 'idx6', 'idx7')
+{
+ my ($cmdret, $stdout, $stderr) = $node->psql('postgres', "SELECT relfilenode"
+ . " FROM pg_catalog.pg_class c"
+ . " WHERE c.relname = '$idx'");
+
+ isnt($stdout, '', 'A relfinode should be returned');
+ isnt($stdout, 0, 'Relfinode should not be 0');
+ $idxs{$idx} = $stdout;
+}
+
+$node->issues_sql_like(
+ [ 'vreindexdb', '--glibc-dependent', 'postgres' ],
+ qr/statement: REINDEX SYSTEM postgres;/,
+ 'reindex system tables');
+
+# get possibly new indexes' relfilenode
+foreach my $idx ('idx1', 'IDX2', 'idx3', 'idx4', 'idx5', 'idx6', 'idx7')
+{
+ my ($cmdret, $stdout, $stderr) = $node->psql('postgres', "SELECT relfilenode"
+ . " FROM pg_catalog.pg_class c"
+ . " WHERE c.relname = '$idx'");
+
+ isnt($stdout, '', 'A relfinode should be returned');
+ isnt($stdout, 0, "Relfinode shouldn't be 0");
+ $newidxs{$idx} = $stdout;
+}
+
+is($idxs{'idx1'}, $newidxs{'idx1'},
+ 'Non string index should not have been reindexed');
+isnt($idxs{'IDX2'}, $newidxs{'IDX2'},
+ 'String index with non specific opclass should have been reindexed');
+is($idxs{'idx3'}, $newidxs{'idx3'},
+ 'String index with specific opclass should not have been reindexed');
+isnt($idxs{'idx4'}, $newidxs{'idx4'},
+ 'String index with mix of opclass should have been reindexed');
+isnt($idxs{'idx5'}, $newidxs{'idx5'},
+ 'String functional index with non specific opclass should have been reindexed');
+is($idxs{'idx6'}, $newidxs{'idx6'},
+ 'String functional index with specific opclass should not have been reindexed');
+isnt($idxs{'idx7'}, $newidxs{'idx7'},
+ 'String functional index with mix of opclass should have been reindexed');
+
+$node->issues_sql_like(
+ [ 'vreindexdb', '--glibc-dependent', '-j2', 'postgres' ],
+ qr/statement: REINDEX SYSTEM postgres;/,
+ 'reindex system tables');
+
+# get possibly new indexes' relfilenode
+foreach my $idx ('idx1', 'IDX2', 'idx3', 'idx4', 'idx5', 'idx6', 'idx7')
+{
+ my ($cmdret, $stdout, $stderr) = $node->psql('postgres', "SELECT relfilenode"
+ . " FROM pg_catalog.pg_class c"
+ . " WHERE c.relname = '$idx'");
+
+ isnt($stdout, '', 'A relfinode should be returned');
+ isnt($stdout, 0, "Relfinode shouldn't be 0");
+ $idxs{$idx} = $stdout;
+}
+
+is($idxs{'idx1'}, $newidxs{'idx1'},
+ 'Non string index should not have been reindexed');
+isnt($idxs{'IDX2'}, $newidxs{'IDX2'},
+ 'String index with non specific opclass should have been reindexed');
+is($idxs{'idx3'}, $newidxs{'idx3'},
+ 'String index with specific opclass should not have been reindexed');
+isnt($idxs{'idx4'}, $newidxs{'idx4'},
+ 'String index with mix of opclass should have been reindexed');
+isnt($idxs{'idx5'}, $newidxs{'idx5'},
+ 'String functional index with non specific opclass should have been reindexed');
+is($idxs{'idx6'}, $newidxs{'idx6'},
+ 'String functional index with specific opclass should not have been reindexed');
+isnt($idxs{'idx7'}, $newidxs{'idx7'},
+ 'String functional index with mix of opclass should have been reindexed');
--
2.20.1
0002-Add-a-SimplePtrList-implementation.patchapplication/octet-stream; name=0002-Add-a-SimplePtrList-implementation.patchDownload
From 4b399b2f9133de4cbcc228a2b1fe2e69b4dfe99a Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 28 Jun 2019 13:32:25 +0200
Subject: [PATCH 2/4] Add a SimplePtrList implementation
---
src/fe_utils/simple_list.c | 21 +++++++++++++++++++++
src/include/fe_utils/simple_list.h | 15 +++++++++++++++
2 files changed, 36 insertions(+)
diff --git a/src/fe_utils/simple_list.c b/src/fe_utils/simple_list.c
index 8d605140a7..4636d24857 100644
--- a/src/fe_utils/simple_list.c
+++ b/src/fe_utils/simple_list.c
@@ -114,3 +114,24 @@ simple_string_list_not_touched(SimpleStringList *list)
}
return NULL;
}
+
+/*
+ * Append a pointer to the list.
+ */
+void
+simple_ptr_list_append(SimplePtrList *list, void *val)
+{
+ SimplePtrListCell *cell;
+
+ cell = (SimplePtrListCell *) pg_malloc(sizeof(SimplePtrListCell));
+ cell->next = NULL;
+ cell->val = val;
+
+ list->size++;
+
+ if (list->tail)
+ list->tail->next = cell;
+ else
+ list->head = cell;
+ list->tail = cell;
+}
diff --git a/src/include/fe_utils/simple_list.h b/src/include/fe_utils/simple_list.h
index 8a95cbb3a8..88b427d04c 100644
--- a/src/include/fe_utils/simple_list.h
+++ b/src/include/fe_utils/simple_list.h
@@ -43,6 +43,19 @@ typedef struct SimpleStringList
SimpleStringListCell *tail;
} SimpleStringList;
+typedef struct SimplePtrListCell
+{
+ struct SimplePtrListCell *next;
+ void *val;
+} SimplePtrListCell;
+
+typedef struct SimplePtrList
+{
+ int size;
+ SimplePtrListCell *head;
+ SimplePtrListCell *tail;
+} SimplePtrList;
+
extern void simple_oid_list_append(SimpleOidList *list, Oid val);
extern bool simple_oid_list_member(SimpleOidList *list, Oid val);
@@ -52,4 +65,6 @@ extern bool simple_string_list_member(SimpleStringList *list, const char *val);
extern const char *simple_string_list_not_touched(SimpleStringList *list);
+extern void simple_ptr_list_append(SimplePtrList *list, void *val);
+
#endif /* SIMPLE_LIST_H */
--
2.20.1
On Sun, Jun 30, 2019 at 11:45:47AM +0200, Julien Rouhaud wrote:
With the glibc 2.28 coming, all users will have to reindex almost
every indexes after a glibc upgrade to guarantee the lack of
corruption. Unfortunately, reindexdb is not ideal for that as it's
processing everything using a single connexion and isn't able to
discard indexes that doesn't depend on a glibc collation.
We have seen that with a database of up to 100GB we finish by cutting
the reindex time from 30 minutes to a couple of minutes with a schema
we work on. Julien, what were the actual numbers?
PFA a patchset to add parallelism to reindexdb (reusing the
infrastructure in vacuumdb with some additions) and an option to
discard indexes that doesn't depend on glibc (without any specific
collation filtering or glibc version detection), with updated
regression tests. Note that this should be applied on top of the
existing reindexdb cleanup & refactoring patch
(https://commitfest.postgresql.org/23/2115/).
Please note that patch 0003 does not seem to apply correctly on HEAD
as of c74d49d4. Here is also a small description of each patch:
- 0001 refactors the connection slot facility from vacuumdb.c into a
new, separate file called parallel.c in src/bin/scripts/. This is not
really fancy as some code only moves around.
- 0002 adds an extra option for simple lists to be able to use
pointers, with an interface to append elements in it.
- 0003 begins to be the actual fancy thing with the addition of a
--jobs option into reindexdb. The main issue here which should be
discussed is that when it comes to reindex of tables, you basically
are not going to have any conflicts between the objects manipulated.
However if you wish to do a reindex on a set of indexes then things
get more tricky as it is necessary to list items per-table so as
multiple connections do not conflict with each other if attempting to
work on multiple indexes of the same table. What this patch does is
to select the set of indexes which need to be worked on (see the
addition of cell in ParallelSlot), and then does a kind of
pre-planning of each item into the connection slots so as each
connection knows from the beginning which items it needs to process.
This is quite different from vacuumdb where a new item is distributed
only on a free connection from a unique list. I'd personally prefer
if we keep the facility in parallel.c so as it is only
execution-dependent and that we have no pre-planning. This would
require keeping within reindexdb.c an array of lists, with one list
corresponding to one connection instead which feels more natural.
- 0004 is the part where the concurrent additions really matter as
this consists in applying an extra filter to the indexes selected so
as only the glibc-sensitive indexes are chosen for the processing.
--
Michael
On Mon, Jul 1, 2019 at 10:55 AM Michael Paquier <michael@paquier.xyz> wrote:
On Sun, Jun 30, 2019 at 11:45:47AM +0200, Julien Rouhaud wrote:
With the glibc 2.28 coming, all users will have to reindex almost
every indexes after a glibc upgrade to guarantee the lack of
corruption. Unfortunately, reindexdb is not ideal for that as it's
processing everything using a single connexion and isn't able to
discard indexes that doesn't depend on a glibc collation.We have seen that with a database of up to 100GB we finish by cutting
the reindex time from 30 minutes to a couple of minutes with a schema
we work on. Julien, what were the actual numbers?
I did my benchmarking using a quite ideal database, having a large
number of tables and various set of indexes, for a 75 GB total size.
This was done on my laptop which has 6 multithreaded cores (and crappy
IO), also keeping the default max_parallel_maintenance_worker = 2.
A naive reindexdb took approximately 33 minutes. Filtering the list
of indexes took that down to slightly less than 15 min, but of course
each database will have a different ratio there.
Then, keeping the --glibc-dependent and using different level of parallelism:
-j1: ~ 14:50
-j3: ~ 7:30
-j6: ~ 5:23
-j8: ~ 4:45
That's pretty much the kind of results I was expecting given the
hardware I used.
PFA a patchset to add parallelism to reindexdb (reusing the
infrastructure in vacuumdb with some additions) and an option to
discard indexes that doesn't depend on glibc (without any specific
collation filtering or glibc version detection), with updated
regression tests. Note that this should be applied on top of the
existing reindexdb cleanup & refactoring patch
(https://commitfest.postgresql.org/23/2115/).Please note that patch 0003 does not seem to apply correctly on HEAD
as of c74d49d4.
Yes, this is because this patchset has to be applied on top of the
reindexdb refactoring patch mentioned. It's sad that we don't have a
good way to deal with that kind of dependency, as it's also breaking
Thomas' cfbot :(
- 0003 begins to be the actual fancy thing with the addition of a
--jobs option into reindexdb. The main issue here which should be
discussed is that when it comes to reindex of tables, you basically
are not going to have any conflicts between the objects manipulated.
However if you wish to do a reindex on a set of indexes then things
get more tricky as it is necessary to list items per-table so as
multiple connections do not conflict with each other if attempting to
work on multiple indexes of the same table. What this patch does is
to select the set of indexes which need to be worked on (see the
addition of cell in ParallelSlot), and then does a kind of
pre-planning of each item into the connection slots so as each
connection knows from the beginning which items it needs to process.
This is quite different from vacuumdb where a new item is distributed
only on a free connection from a unique list. I'd personally prefer
if we keep the facility in parallel.c so as it is only
execution-dependent and that we have no pre-planning. This would
require keeping within reindexdb.c an array of lists, with one list
corresponding to one connection instead which feels more natural.
My fear here is that this approach would add some extra complexity,
especially requiring to deal with free connection handling both in
GetIdleSlot() and the main reindexdb loop. Also, the pre-planning
allows us to start processing the biggest tables first, which
optimises the overall runtime.
Now that we have REINDEX CONCURRENTLY, I think reindexdb is going to
gain more popularity.
Please don't reuse a file name as generic as "parallel.c" -- it's
annoying when navigating source. Maybe conn_parallel.c multiconn.c
connscripts.c admconnection.c ...?
If your server crashes or is stopped midway during the reindex, you
would have to start again from scratch, and it's tedious (if it's
possible at all) to determine which indexes were missed. I think it
would be useful to have a two-phase mode: in the initial phase reindexdb
computes the list of indexes to be reindexed and saves them into a work
table somewhere. In the second phase, it reads indexes from that table
and processes them, marking them as done in the work table. If the
second phase crashes or is stopped, it can be restarted and consults the
work table. I would keep the work table, as it provides a bit of an
audit trail. It may be important to be able to run even if unable to
create such a work table (because of the <ironic>numerous</> users that
DROP DATABASE postgres).
Maybe we'd have two flags in the work table for each index:
"reindex requested", "reindex done".
The "glibc filter" thing (which I take to mean "indexes that depend on
collations") would apply to the first phase: it just skips adding other
indexes to the work table. I suppose ICU collations are not affected,
so the filter would be for glibc collations only? The --glibc-dependent
switch seems too ad-hoc. Maybe "--exclude-rule=glibc"? That way we can
add other rules later. (Not "--exclude=foo" because we'll want to add
the possibility to ignore specific indexes by name.)
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Michael Paquier <michael@paquier.xyz> writes:
- 0003 begins to be the actual fancy thing with the addition of a
--jobs option into reindexdb. The main issue here which should be
discussed is that when it comes to reindex of tables, you basically
are not going to have any conflicts between the objects manipulated.
However if you wish to do a reindex on a set of indexes then things
get more tricky as it is necessary to list items per-table so as
multiple connections do not conflict with each other if attempting to
work on multiple indexes of the same table. What this patch does is
to select the set of indexes which need to be worked on (see the
addition of cell in ParallelSlot), and then does a kind of
pre-planning of each item into the connection slots so as each
connection knows from the beginning which items it needs to process.
This is quite different from vacuumdb where a new item is distributed
only on a free connection from a unique list. I'd personally prefer
if we keep the facility in parallel.c so as it is only
execution-dependent and that we have no pre-planning. This would
require keeping within reindexdb.c an array of lists, with one list
corresponding to one connection instead which feels more natural.
Couldn't we make this enormously simpler and less bug-prone by just
dictating that --jobs applies only to reindex-table operations?
- 0004 is the part where the concurrent additions really matter as
this consists in applying an extra filter to the indexes selected so
as only the glibc-sensitive indexes are chosen for the processing.
I think you'd be better off to define and document this as "reindex
only collation-sensitive indexes", without any particular reference
to a reason why somebody might want to do that.
regards, tom lane
On Mon, Jul 1, 2019 at 3:51 PM Alvaro Herrera <alvherre@2ndquadrant.com> wrote:
Please don't reuse a file name as generic as "parallel.c" -- it's
annoying when navigating source. Maybe conn_parallel.c multiconn.c
connscripts.c admconnection.c ...?
I could use scripts_parallel.[ch] as I've already used it in the #define part?
If your server crashes or is stopped midway during the reindex, you
would have to start again from scratch, and it's tedious (if it's
possible at all) to determine which indexes were missed. I think it
would be useful to have a two-phase mode: in the initial phase reindexdb
computes the list of indexes to be reindexed and saves them into a work
table somewhere. In the second phase, it reads indexes from that table
and processes them, marking them as done in the work table. If the
second phase crashes or is stopped, it can be restarted and consults the
work table. I would keep the work table, as it provides a bit of an
audit trail. It may be important to be able to run even if unable to
create such a work table (because of the <ironic>numerous</> users that
DROP DATABASE postgres).
Or we could create a table locally in each database, that would fix
this problem and probably make the code simpler?
It also raises some additional concerns about data expiration. I
guess that someone could launch the tool by mistake, kill reindexdb,
and run it again 2 months later while a lot of new objects have been
added for instance.
The "glibc filter" thing (which I take to mean "indexes that depend on
collations") would apply to the first phase: it just skips adding other
indexes to the work table. I suppose ICU collations are not affected,
so the filter would be for glibc collations only?
Indeed, ICU shouldn't need such a filter. xxx_pattern_ops based
indexes are also excluded.
The --glibc-dependent
switch seems too ad-hoc. Maybe "--exclude-rule=glibc"? That way we can
add other rules later. (Not "--exclude=foo" because we'll want to add
the possibility to ignore specific indexes by name.)
That's a good point, I like the --exclude-rule switch.
On Mon, Jul 1, 2019 at 4:10 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Michael Paquier <michael@paquier.xyz> writes:
- 0003 begins to be the actual fancy thing with the addition of a
--jobs option into reindexdb. The main issue here which should be
discussed is that when it comes to reindex of tables, you basically
are not going to have any conflicts between the objects manipulated.
However if you wish to do a reindex on a set of indexes then things
get more tricky as it is necessary to list items per-table so as
multiple connections do not conflict with each other if attempting to
work on multiple indexes of the same table. What this patch does is
to select the set of indexes which need to be worked on (see the
addition of cell in ParallelSlot), and then does a kind of
pre-planning of each item into the connection slots so as each
connection knows from the beginning which items it needs to process.
This is quite different from vacuumdb where a new item is distributed
only on a free connection from a unique list. I'd personally prefer
if we keep the facility in parallel.c so as it is only
execution-dependent and that we have no pre-planning. This would
require keeping within reindexdb.c an array of lists, with one list
corresponding to one connection instead which feels more natural.Couldn't we make this enormously simpler and less bug-prone by just
dictating that --jobs applies only to reindex-table operations?
That would also mean that we'll have to fallback on doing reindex at
table-level, even if we only want to reindex indexes that depends on
glibc. I'm afraid that this will often add a huge penalty.
- 0004 is the part where the concurrent additions really matter as
this consists in applying an extra filter to the indexes selected so
as only the glibc-sensitive indexes are chosen for the processing.I think you'd be better off to define and document this as "reindex
only collation-sensitive indexes", without any particular reference
to a reason why somebody might want to do that.
We should still document that indexes based on ICU would be exluded?
I also realize that I totally forgot to update reindexdb.sgml. Sorry
about that, I'll fix with the next versions.
Julien Rouhaud wrote:
I think you'd be better off to define and document this as "reindex
only collation-sensitive indexes", without any particular reference
to a reason why somebody might want to do that.We should still document that indexes based on ICU would be exluded?
But why exclude them?
As a data point, in the last 5 years, the en_US collation in ICU
had 7 different versions (across 11 major versions of ICU):
ICU Unicode en_US
54.1 7.0 137.56
55.1 7.0 153.56
56.1 8.0 153.64
57.1 8.0 153.64
58.2 9.0 153.72
59.1 9.0 153.72
60.2 10.0 153.80
61.1 10.0 153.80
62.1 11.0 153.88
63.2 11.0 153.88
64.2 12.1 153.97
The rightmost column corresponds to pg_collation.collversion
in Postgres.
Each time there's a new Unicode version, it seems
all collation versions are bumped. And there's a new Unicode
version pretty much every year these days.
Based on this, most ICU upgrades in practice would require reindexing
affected indexes.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
On 2019-Jul-01, Daniel Verite wrote:
But why exclude them?
As a data point, in the last 5 years, the en_US collation in ICU
had 7 different versions (across 11 major versions of ICU):
So we need a switch --include-rule=icu-collations?
(I mentioned "--exclude-rule=glibc" elsewhere in the thread, but I think
it should be --include-rule=glibc-collations instead, no?)
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Mon, Jul 1, 2019 at 10:13 PM Daniel Verite <daniel@manitou-mail.org> wrote:
I think you'd be better off to define and document this as "reindex
only collation-sensitive indexes", without any particular reference
to a reason why somebody might want to do that.We should still document that indexes based on ICU would be exluded?
But why exclude them?
As a data point, in the last 5 years, the en_US collation in ICU
had 7 different versions (across 11 major versions of ICU):ICU Unicode en_US
54.1 7.0 137.56
55.1 7.0 153.56
56.1 8.0 153.64
57.1 8.0 153.64
58.2 9.0 153.72
59.1 9.0 153.72
60.2 10.0 153.80
61.1 10.0 153.80
62.1 11.0 153.88
63.2 11.0 153.88
64.2 12.1 153.97The rightmost column corresponds to pg_collation.collversion
in Postgres.
Each time there's a new Unicode version, it seems
all collation versions are bumped. And there's a new Unicode
version pretty much every year these days.
Based on this, most ICU upgrades in practice would require reindexing
affected indexes.
I have a vague recollection that ICU was providing some backward
compatibility so that even if you upgrade your lib you can still get
the sort order that was active when you built your indexes, though
maybe for a limited number of versions.
Even if that's just me being delusional, I'd still prefer Alvaro's
approach to have distinct switches for each collation system.
On Mon, Jul 1, 2019 at 1:34 PM Julien Rouhaud <rjuju123@gmail.com> wrote:
I have a vague recollection that ICU was providing some backward
compatibility so that even if you upgrade your lib you can still get
the sort order that was active when you built your indexes, though
maybe for a limited number of versions.
That isn't built in. Another database system that uses ICU handles
this by linking to multiple versions of ICU, each with its own UCA
version and associated collations. I don't think that we want to go
there, so it makes sense to make an upgrade that crosses ICU or glibc
versions as painless as possible.
Note that ICU does at least provide a standard way to use multiple
versions at once; the symbol names have the ICU version baked in.
You're actually calling the functions using the versioned symbol names
without realizing it, because there is macro trickery involved.
--
Peter Geoghegan
On Tue, Jul 2, 2019 at 8:34 AM Julien Rouhaud <rjuju123@gmail.com> wrote:
Even if that's just me being delusional, I'd still prefer Alvaro's
approach to have distinct switches for each collation system.
Hi Julien,
Makes sense. But why use the name "glibc" in the code and user
interface? The name of the collation provider in PostgreSQL is "libc"
(for example in the CREATE COLLATION command), and the problem applies
no matter who makes your libc.
--
Thomas Munro
https://enterprisedb.com
On 2019-Jul-02, Thomas Munro wrote:
On Tue, Jul 2, 2019 at 8:34 AM Julien Rouhaud <rjuju123@gmail.com> wrote:
Even if that's just me being delusional, I'd still prefer Alvaro's
approach to have distinct switches for each collation system.Hi Julien,
Makes sense. But why use the name "glibc" in the code and user
interface? The name of the collation provider in PostgreSQL is "libc"
(for example in the CREATE COLLATION command), and the problem applies
no matter who makes your libc.
Makes sense. "If your libc is glibc and you go across an upgrade over
version X, please use --include-rule=libc-collation"
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Mon, Jul 01, 2019 at 06:28:13PM +0200, Julien Rouhaud wrote:
On Mon, Jul 1, 2019 at 4:10 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Couldn't we make this enormously simpler and less bug-prone by just
dictating that --jobs applies only to reindex-table operations?
I had the same argument about the first patch sets actually, but... :)
That would also mean that we'll have to fallback on doing reindex at
table-level, even if we only want to reindex indexes that depends on
glibc. I'm afraid that this will often add a huge penalty.
Yes, I would expect that most of the time glibc-sensible indexes are
also mixed with other ones which we don't care about here. One
advantage of the argument from Tom though is that it is possible to
introduce --jobs with minimal steps:
1) Refactor the code for connection slots, without the cell addition
2) Introduce --jobs without INDEX support.
In short, the conflict business between indexes is something which
could be tackled afterwards and with a separate patch. Parallel
indexes at table-level has value in itself, particularly with
CONCURRENTLY coming in the picture.
--
Michael
On Mon, Jul 01, 2019 at 06:14:20PM +0200, Julien Rouhaud wrote:
On Mon, Jul 1, 2019 at 3:51 PM Alvaro Herrera <alvherre@2ndquadrant.com> wrote:
Please don't reuse a file name as generic as "parallel.c" -- it's
annoying when navigating source. Maybe conn_parallel.c multiconn.c
connscripts.c admconnection.c ...?I could use scripts_parallel.[ch] as I've already used it in the
#define part?
multiconn.c sounds rather good, but I have a poor ear for any kind of
naming..
If your server crashes or is stopped midway during the reindex, you
would have to start again from scratch, and it's tedious (if it's
possible at all) to determine which indexes were missed. I think it
would be useful to have a two-phase mode: in the initial phase reindexdb
computes the list of indexes to be reindexed and saves them into a work
table somewhere. In the second phase, it reads indexes from that table
and processes them, marking them as done in the work table. If the
second phase crashes or is stopped, it can be restarted and consults the
work table. I would keep the work table, as it provides a bit of an
audit trail. It may be important to be able to run even if unable to
create such a work table (because of the <ironic>numerous</> users that
DROP DATABASE postgres).Or we could create a table locally in each database, that would fix
this problem and probably make the code simpler?It also raises some additional concerns about data expiration. I
guess that someone could launch the tool by mistake, kill reindexdb,
and run it again 2 months later while a lot of new objects have been
added for instance.
This looks like fancy additions, still that's not the core of the
problem, no? If you begin to play in this area you would need more
control options, basically a "continue" mode to be able to restart a
previously failed attempt, and a "reinit" mode able to restart the
operation completely from scratch, and perhaps even a "reset" mode
which cleans up any data already present. Not really a complexity,
but this has to be maintained a database level.
The --glibc-dependent
switch seems too ad-hoc. Maybe "--exclude-rule=glibc"? That way we can
add other rules later. (Not "--exclude=foo" because we'll want to add
the possibility to ignore specific indexes by name.)That's a good point, I like the --exclude-rule switch.
Sounds kind of nice.
--
Michael
On 2019-07-01 22:46, Alvaro Herrera wrote:
On 2019-Jul-02, Thomas Munro wrote:
On Tue, Jul 2, 2019 at 8:34 AM Julien Rouhaud <rjuju123@gmail.com> wrote:
Even if that's just me being delusional, I'd still prefer Alvaro's
approach to have distinct switches for each collation system.Makes sense. But why use the name "glibc" in the code and user
interface? The name of the collation provider in PostgreSQL is "libc"
(for example in the CREATE COLLATION command), and the problem applies
no matter who makes your libc.Makes sense. "If your libc is glibc and you go across an upgrade over
version X, please use --include-rule=libc-collation"
I think it might be better to put the logic of what indexes are
collation affected etc. into the backend REINDEX command. We are likely
to enhance the collation version and dependency tracking over time,
possibly soon, possibly multiple times, and it would be very cumbersome
to have to keep updating reindexdb with this. Moreover, since for
performance you likely want to reindex by table, implementing a logic of
"reindex all collation-affected indexes on this table" would be much
easier to do in the backend.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Mon, Jul 1, 2019 at 11:21 PM Peter Geoghegan <pg@bowt.ie> wrote:
On Mon, Jul 1, 2019 at 1:34 PM Julien Rouhaud <rjuju123@gmail.com> wrote:
I have a vague recollection that ICU was providing some backward
compatibility so that even if you upgrade your lib you can still get
the sort order that was active when you built your indexes, though
maybe for a limited number of versions.That isn't built in. Another database system that uses ICU handles
this by linking to multiple versions of ICU, each with its own UCA
version and associated collations. I don't think that we want to go
there, so it makes sense to make an upgrade that crosses ICU or glibc
versions as painless as possible.Note that ICU does at least provide a standard way to use multiple
versions at once; the symbol names have the ICU version baked in.
You're actually calling the functions using the versioned symbol names
without realizing it, because there is macro trickery involved.
Ah, thanks for the clarification!
On Sun, Jun 30, 2019 at 11:45:47AM +0200, Julien Rouhaud wrote:
Hi,
With the glibc 2.28 coming, all users will have to reindex almost
every indexes after a glibc upgrade to guarantee the lack of
corruption. Unfortunately, reindexdb is not ideal for that as it's
processing everything using a single connexion and isn't able to
discard indexes that doesn't depend on a glibc collation.PFA a patchset to add parallelism to reindexdb (reusing the
infrastructure in vacuumdb with some additions) and an option to
discard indexes that doesn't depend on glibc (without any specific
collation filtering or glibc version detection), with updated
regression tests. Note that this should be applied on top of the
existing reindexdb cleanup & refactoring patch
(https://commitfest.postgresql.org/23/2115/).This was sponsored by VMware, and has been discussed internally with
Kevin and Michael, in Cc.
I wonder why this is necessary:
pg_log_error("cannot reindex glibc dependent objects and a subset of objects");
What's the reasoning behind that? It seems like a valid use case to me -
imagine you have a bug database, but only a couple of tables are used by
the application regularly (the rest may be archive tables, for example).
Why not to allow rebuilding glibc-dependent indexes on the used tables, so
that the database can be opened for users sooner.
BTW now that we allow rebuilding only some of the indexes, it'd be great
to have a dry-run mode, were we just print which indexes will be rebuilt
without actually rebuilding them.
regards
--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Tue, Jul 2, 2019 at 9:19 AM Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
On 2019-07-01 22:46, Alvaro Herrera wrote:
On 2019-Jul-02, Thomas Munro wrote:
On Tue, Jul 2, 2019 at 8:34 AM Julien Rouhaud <rjuju123@gmail.com> wrote:
Even if that's just me being delusional, I'd still prefer Alvaro's
approach to have distinct switches for each collation system.Makes sense. But why use the name "glibc" in the code and user
interface? The name of the collation provider in PostgreSQL is "libc"
(for example in the CREATE COLLATION command), and the problem applies
no matter who makes your libc.Makes sense. "If your libc is glibc and you go across an upgrade over
version X, please use --include-rule=libc-collation"I think it might be better to put the logic of what indexes are
collation affected etc. into the backend REINDEX command. We are likely
to enhance the collation version and dependency tracking over time,
possibly soon, possibly multiple times, and it would be very cumbersome
to have to keep updating reindexdb with this. Moreover, since for
performance you likely want to reindex by table, implementing a logic of
"reindex all collation-affected indexes on this table" would be much
easier to do in the backend.
That's a great idea, and would make the parallelism in reindexdb much
simpler. There's however a downside, as users won't have a way to
benefit from index filtering until they upgrade to this version. OTOH
glibc 2.28 is already there, and a hypothetical fancy reindexdb is far
from being released.
On Tue, Jul 2, 2019 at 10:28 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:
I wonder why this is necessary:
pg_log_error("cannot reindex glibc dependent objects and a subset of objects");
What's the reasoning behind that? It seems like a valid use case to me -
imagine you have a bug database, but only a couple of tables are used by
the application regularly (the rest may be archive tables, for example).
Why not to allow rebuilding glibc-dependent indexes on the used tables, so
that the database can be opened for users sooner.
It just seemed wrong to me to allow a partial processing for something
that's aimed to prevent corruption. I'd think that if users are
knowledgeable enough to only reindex a subset of indexes/tables in
such cases, they can also discard indexes that don't get affected by a
collation lib upgrade. I'm not strongly opposed to supporting if
though, as there indeed can be valid use cases.
BTW now that we allow rebuilding only some of the indexes, it'd be great
to have a dry-run mode, were we just print which indexes will be rebuilt
without actually rebuilding them.
+1. If we end up doing the filter in the backend, we'd have to add
such option in the REINDEX command, and actually issue all the orders
to retrieve the list.
On Tue, Jul 02, 2019 at 10:45:44AM +0200, Julien Rouhaud wrote:
On Tue, Jul 2, 2019 at 10:28 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:I wonder why this is necessary:
pg_log_error("cannot reindex glibc dependent objects and a subset of objects");
What's the reasoning behind that? It seems like a valid use case to me -
imagine you have a bug database, but only a couple of tables are used by
the application regularly (the rest may be archive tables, for example).
Why not to allow rebuilding glibc-dependent indexes on the used tables, so
that the database can be opened for users sooner.It just seemed wrong to me to allow a partial processing for something
that's aimed to prevent corruption. I'd think that if users are
knowledgeable enough to only reindex a subset of indexes/tables in
such cases, they can also discard indexes that don't get affected by a
collation lib upgrade. I'm not strongly opposed to supporting if
though, as there indeed can be valid use cases.
I don't know, it just seems like an unnecessary limitation.
BTW now that we allow rebuilding only some of the indexes, it'd be great
to have a dry-run mode, were we just print which indexes will be rebuilt
without actually rebuilding them.+1. If we end up doing the filter in the backend, we'd have to add
such option in the REINDEX command, and actually issue all the orders
to retrieve the list.
Hmmm, yeah. FWIW I'm not requesting v0 to have that feature, but it'd be
good to design the feature in a way that allows adding it later.
regards
--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On 2019-07-02 10:30, Julien Rouhaud wrote:
That's a great idea, and would make the parallelism in reindexdb much
simpler. There's however a downside, as users won't have a way to
benefit from index filtering until they upgrade to this version. OTOH
glibc 2.28 is already there, and a hypothetical fancy reindexdb is far
from being released.
Isn't that also the case for your proposal? We are not going to release
a new reindexdb before a new REINDEX.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On 2019-07-02 10:45, Julien Rouhaud wrote:
It just seemed wrong to me to allow a partial processing for something
that's aimed to prevent corruption. I'd think that if users are
knowledgeable enough to only reindex a subset of indexes/tables in
such cases, they can also discard indexes that don't get affected by a
collation lib upgrade. I'm not strongly opposed to supporting if
though, as there indeed can be valid use cases.
We are moving in this direction. Thomas Munro has proposed an approach
for tracking collation versions on a per-object level rather than
per-database. So then we'd need a way to reindex not those indexes
affected by collation but only those affected by collation and not yet
fixed.
One could also imagine a behavior where not-yet-fixed indexes are simply
ignored by the planner. So the gradual upgrading approach that Tomas
described is absolutely a possibility.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Fri, Jul 5, 2019 at 6:16 PM Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
On 2019-07-02 10:30, Julien Rouhaud wrote:
That's a great idea, and would make the parallelism in reindexdb much
simpler. There's however a downside, as users won't have a way to
benefit from index filtering until they upgrade to this version. OTOH
glibc 2.28 is already there, and a hypothetical fancy reindexdb is far
from being released.Isn't that also the case for your proposal? We are not going to release
a new reindexdb before a new REINDEX.
Sure, but my point was that once the new reindexdb is released (or if
you're so desperate, using a nightly build or compiling your own), it
can be used against any previous major version. There is probably a
large fraction of users who don't perform a postgres upgrade when they
upgrade their OS, so that's IMHO also something to consider.
On Fri, Jul 05, 2019 at 07:25:41PM +0200, Julien Rouhaud wrote:
On Fri, Jul 5, 2019 at 6:16 PM Peter Eisentraut <peter.eisentraut@2ndquadrant.com> wrote:
Isn't that also the case for your proposal? We are not going to release
a new reindexdb before a new REINDEX.Sure, but my point was that once the new reindexdb is released (or if
you're so desperate, using a nightly build or compiling your own), it
can be used against any previous major version. There is probably a
large fraction of users who don't perform a postgres upgrade when they
upgrade their OS, so that's IMHO also something to consider.
I think that we need to think long-term here and be confident in the
fact we will still see breakages with collations and glibc, using a
solution that we think is the right API. Peter's idea to make the
backend-aware command of the filtering is cool. On top of that, there
is no need to add any conflict logic in reindexdb and we can live with
restricting --jobs support for non-index objects.
--
Michael
On Mon, Jul 8, 2019 at 9:57 AM Michael Paquier <michael@paquier.xyz> wrote:
On Fri, Jul 05, 2019 at 07:25:41PM +0200, Julien Rouhaud wrote:
On Fri, Jul 5, 2019 at 6:16 PM Peter Eisentraut <peter.eisentraut@2ndquadrant.com> wrote:
Isn't that also the case for your proposal? We are not going to release
a new reindexdb before a new REINDEX.Sure, but my point was that once the new reindexdb is released (or if
you're so desperate, using a nightly build or compiling your own), it
can be used against any previous major version. There is probably a
large fraction of users who don't perform a postgres upgrade when they
upgrade their OS, so that's IMHO also something to consider.I think that we need to think long-term here and be confident in the
fact we will still see breakages with collations and glibc, using a
solution that we think is the right API. Peter's idea to make the
backend-aware command of the filtering is cool. On top of that, there
is no need to add any conflict logic in reindexdb and we can live with
restricting --jobs support for non-index objects.
Don't get me wrong, I do agree that implementing filtering in the
backend is a better design. What's bothering me is that I also agree
that there will be more glibc breakage, and if that happens within a
few years, a lot of people will still be using pg12- version, and they
still won't have an efficient way to rebuild their indexes. Now, it'd
be easy to publish an external tools that does a simple
parallel-and-glic-filtering reindex tool that will serve that purpose
for the few years it'll be needed, so everyone can be happy.
For now, I'll resubmit the parallel patch using per-table only
approach, and will submit the filtering in the backend using a new
REINDEX option in a different thread.
On Mon, Jul 8, 2019 at 9:08 PM Julien Rouhaud <rjuju123@gmail.com> wrote:
I'll resubmit the parallel patch using per-table only
approach
Attached.
Attachments:
0001-Export-vacuumdb-s-parallel-infrastructure.patchapplication/octet-stream; name=0001-Export-vacuumdb-s-parallel-infrastructure.patchDownload
From efe1d1846369dfb5b69148d81ccb917427a22e7d Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 28 Jun 2019 13:11:29 +0200
Subject: [PATCH 1/2] Export vacuumdb's parallel infrastructure
---
src/bin/scripts/Makefile | 4 +-
src/bin/scripts/scripts_parallel.c | 282 ++++++++++++++++++++++++++++
src/bin/scripts/scripts_parallel.h | 34 ++++
src/bin/scripts/vacuumdb.c | 284 +----------------------------
4 files changed, 319 insertions(+), 285 deletions(-)
create mode 100644 src/bin/scripts/scripts_parallel.c
create mode 100644 src/bin/scripts/scripts_parallel.h
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 9f352b5e2b..3cd793b134 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -28,7 +28,7 @@ createuser: createuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport
dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-vacuumdb: vacuumdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
@@ -50,7 +50,7 @@ uninstall:
clean distclean maintainer-clean:
rm -f $(addsuffix $(X), $(PROGRAMS)) $(addsuffix .o, $(PROGRAMS))
- rm -f common.o $(WIN32RES)
+ rm -f common.o scripts_parallel.o $(WIN32RES)
rm -rf tmp_check
check:
diff --git a/src/bin/scripts/scripts_parallel.c b/src/bin/scripts/scripts_parallel.c
new file mode 100644
index 0000000000..bcaed0df34
--- /dev/null
+++ b/src/bin/scripts/scripts_parallel.c
@@ -0,0 +1,282 @@
+/*-------------------------------------------------------------------------
+ *
+ * scripts_parallel.c
+ * Parallel support for bin/scripts/
+ *
+ *
+ * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/bin/scripts/scripts_parallel.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#ifdef HAVE_SYS_SELECT_H
+#include <sys/select.h>
+#endif
+
+#include "common.h"
+#include "common/logging.h"
+#include "scripts_parallel.h"
+
+#define ERRCODE_UNDEFINED_TABLE "42P01"
+
+
+/*
+ * GetIdleSlot
+ * Return a connection slot that is ready to execute a command.
+ *
+ * We return the first slot we find that is marked isFree, if one is;
+ * otherwise, we loop on select() until one socket becomes available. When
+ * this happens, we read the whole set and mark as free all sockets that become
+ * available.
+ *
+ * If an error occurs, NULL is returned.
+ */
+ParallelSlot *
+GetIdleSlot(ParallelSlot slots[], int numslots,
+ const char *progname)
+{
+ int i;
+ int firstFree = -1;
+
+ /* Any connection already known free? */
+ for (i = 0; i < numslots; i++)
+ {
+ if (slots[i].isFree)
+ return slots + i;
+ }
+
+ /*
+ * No free slot found, so wait until one of the connections has finished
+ * its task and return the available slot.
+ */
+ while (firstFree < 0)
+ {
+ fd_set slotset;
+ int maxFd = 0;
+ bool aborting;
+
+ /* We must reconstruct the fd_set for each call to select_loop */
+ FD_ZERO(&slotset);
+
+ for (i = 0; i < numslots; i++)
+ {
+ int sock = PQsocket(slots[i].connection);
+
+ /*
+ * We don't really expect any connections to lose their sockets
+ * after startup, but just in case, cope by ignoring them.
+ */
+ if (sock < 0)
+ continue;
+
+ FD_SET(sock, &slotset);
+ if (sock > maxFd)
+ maxFd = sock;
+ }
+
+ SetCancelConn(slots->connection);
+ i = select_loop(maxFd, &slotset, &aborting);
+ ResetCancelConn();
+
+ if (aborting)
+ {
+ /*
+ * We set the cancel-receiving connection to the one in the zeroth
+ * slot above, so fetch the error from there.
+ */
+ GetQueryResult(slots->connection, progname);
+ return NULL;
+ }
+ Assert(i != 0);
+
+ for (i = 0; i < numslots; i++)
+ {
+ int sock = PQsocket(slots[i].connection);
+
+ if (sock >= 0 && FD_ISSET(sock, &slotset))
+ {
+ /* select() says input is available, so consume it */
+ PQconsumeInput(slots[i].connection);
+ }
+
+ /* Collect result(s) as long as any are available */
+ while (!PQisBusy(slots[i].connection))
+ {
+ PGresult *result = PQgetResult(slots[i].connection);
+
+ if (result != NULL)
+ {
+ /* Check and discard the command result */
+ if (!ProcessQueryResult(slots[i].connection, result,
+ progname))
+ return NULL;
+ }
+ else
+ {
+ /* This connection has become idle */
+ slots[i].isFree = true;
+ if (firstFree < 0)
+ firstFree = i;
+ break;
+ }
+ }
+ }
+ }
+
+ return slots + firstFree;
+}
+
+/*
+ * ProcessQueryResult
+ *
+ * Process (and delete) a query result. Returns true if there's no error,
+ * false otherwise -- but errors about trying to vacuum a missing relation
+ * are reported and subsequently ignored.
+ */
+bool
+ProcessQueryResult(PGconn *conn, PGresult *result, const char *progname)
+{
+ /*
+ * If it's an error, report it. Errors about a missing table are harmless
+ * so we continue processing; but die for other errors.
+ */
+ if (PQresultStatus(result) != PGRES_COMMAND_OK)
+ {
+ char *sqlState = PQresultErrorField(result, PG_DIAG_SQLSTATE);
+
+ pg_log_error("processing of database \"%s\" failed: %s",
+ PQdb(conn), PQerrorMessage(conn));
+
+ if (sqlState && strcmp(sqlState, ERRCODE_UNDEFINED_TABLE) != 0)
+ {
+ PQclear(result);
+ return false;
+ }
+ }
+
+ PQclear(result);
+ return true;
+}
+
+/*
+ * GetQueryResult
+ *
+ * Pump the conn till it's dry of results; return false if any are errors.
+ * Note that this will block if the conn is busy.
+ */
+bool
+GetQueryResult(PGconn *conn, const char *progname)
+{
+ bool ok = true;
+ PGresult *result;
+
+ SetCancelConn(conn);
+ while ((result = PQgetResult(conn)) != NULL)
+ {
+ if (!ProcessQueryResult(conn, result, progname))
+ ok = false;
+ }
+ ResetCancelConn();
+ return ok;
+}
+
+/*
+ * DisconnectDatabase
+ * Disconnect the connection associated with the given slot
+ */
+void
+DisconnectDatabase(ParallelSlot *slot)
+{
+ char errbuf[256];
+
+ if (!slot->connection)
+ return;
+
+ if (PQtransactionStatus(slot->connection) == PQTRANS_ACTIVE)
+ {
+ PGcancel *cancel;
+
+ if ((cancel = PQgetCancel(slot->connection)))
+ {
+ (void) PQcancel(cancel, errbuf, sizeof(errbuf));
+ PQfreeCancel(cancel);
+ }
+ }
+
+ PQfinish(slot->connection);
+ slot->connection = NULL;
+}
+
+/*
+ * Loop on select() until a descriptor from the given set becomes readable.
+ *
+ * If we get a cancel request while we're waiting, we forego all further
+ * processing and set the *aborting flag to true. The return value must be
+ * ignored in this case. Otherwise, *aborting is set to false.
+ */
+int
+select_loop(int maxFd, fd_set *workerset, bool *aborting)
+{
+ int i;
+ fd_set saveSet = *workerset;
+
+ if (CancelRequested)
+ {
+ *aborting = true;
+ return -1;
+ }
+ else
+ *aborting = false;
+
+ for (;;)
+ {
+ /*
+ * On Windows, we need to check once in a while for cancel requests;
+ * on other platforms we rely on select() returning when interrupted.
+ */
+ struct timeval *tvp;
+#ifdef WIN32
+ struct timeval tv = {0, 1000000};
+
+ tvp = &tv;
+#else
+ tvp = NULL;
+#endif
+
+ *workerset = saveSet;
+ i = select(maxFd + 1, workerset, NULL, NULL, tvp);
+
+#ifdef WIN32
+ if (i == SOCKET_ERROR)
+ {
+ i = -1;
+
+ if (WSAGetLastError() == WSAEINTR)
+ errno = EINTR;
+ }
+#endif
+
+ if (i < 0 && errno == EINTR)
+ continue; /* ignore this */
+ if (i < 0 || CancelRequested)
+ *aborting = true; /* but not this */
+ if (i == 0)
+ continue; /* timeout (Win32 only) */
+ break;
+ }
+
+ return i;
+}
+
+void
+init_slot(ParallelSlot *slot, PGconn *conn)
+{
+ slot->connection = conn;
+ /* Initially assume connection is idle */
+ slot->isFree = true;
+}
diff --git a/src/bin/scripts/scripts_parallel.h b/src/bin/scripts/scripts_parallel.h
new file mode 100644
index 0000000000..b0e841cffb
--- /dev/null
+++ b/src/bin/scripts/scripts_parallel.h
@@ -0,0 +1,34 @@
+/*
+ * scripts_parallel.h
+ * Parallel support for bin/scripts/
+ *
+ * Copyright (c) 2003-2019, PostgreSQL Global Development Group
+ *
+ * src/bin/scripts/scripts_parallel.h
+ */
+#ifndef SCRIPTS_PARALLEL_H
+#define SCRIPTS_PARALLEL_H
+
+/* Parallel processing stuff */
+typedef struct ParallelSlot
+{
+ PGconn *connection; /* One connection */
+ bool isFree; /* Is it known to be idle? */
+} ParallelSlot;
+
+extern ParallelSlot *GetIdleSlot(ParallelSlot slots[], int numslots,
+ const char *progname);
+
+extern bool ProcessQueryResult(PGconn *conn, PGresult *result,
+ const char *progname);
+
+extern bool GetQueryResult(PGconn *conn, const char *progname);
+
+extern void DisconnectDatabase(ParallelSlot *slot);
+
+extern int select_loop(int maxFd, fd_set *workerset, bool *aborting);
+
+extern void init_slot(ParallelSlot *slot, PGconn *conn);
+
+
+#endif /* SCRIPTS_PARALLEL_H */
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 3bcd14b4dc..a7dea1cef8 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -12,10 +12,6 @@
#include "postgres_fe.h"
-#ifdef HAVE_SYS_SELECT_H
-#include <sys/select.h>
-#endif
-
#include "catalog/pg_class_d.h"
#include "common.h"
@@ -23,17 +19,9 @@
#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
-#define ERRCODE_UNDEFINED_TABLE "42P01"
-
-/* Parallel vacuuming stuff */
-typedef struct ParallelSlot
-{
- PGconn *connection; /* One connection */
- bool isFree; /* Is it known to be idle? */
-} ParallelSlot;
-
/* vacuum options controlled by user flags */
typedef struct vacuumingOptions
{
@@ -71,20 +59,6 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
const char *table, const char *progname, bool async);
-static ParallelSlot *GetIdleSlot(ParallelSlot slots[], int numslots,
- const char *progname);
-
-static bool ProcessQueryResult(PGconn *conn, PGresult *result,
- const char *progname);
-
-static bool GetQueryResult(PGconn *conn, const char *progname);
-
-static void DisconnectDatabase(ParallelSlot *slot);
-
-static int select_loop(int maxFd, fd_set *workerset, bool *aborting);
-
-static void init_slot(ParallelSlot *slot, PGconn *conn);
-
static void help(const char *progname);
/* For analyze-in-stages mode */
@@ -953,262 +927,6 @@ run_vacuum_command(PGconn *conn, const char *sql, bool echo,
}
}
-/*
- * GetIdleSlot
- * Return a connection slot that is ready to execute a command.
- *
- * We return the first slot we find that is marked isFree, if one is;
- * otherwise, we loop on select() until one socket becomes available. When
- * this happens, we read the whole set and mark as free all sockets that become
- * available.
- *
- * If an error occurs, NULL is returned.
- */
-static ParallelSlot *
-GetIdleSlot(ParallelSlot slots[], int numslots,
- const char *progname)
-{
- int i;
- int firstFree = -1;
-
- /* Any connection already known free? */
- for (i = 0; i < numslots; i++)
- {
- if (slots[i].isFree)
- return slots + i;
- }
-
- /*
- * No free slot found, so wait until one of the connections has finished
- * its task and return the available slot.
- */
- while (firstFree < 0)
- {
- fd_set slotset;
- int maxFd = 0;
- bool aborting;
-
- /* We must reconstruct the fd_set for each call to select_loop */
- FD_ZERO(&slotset);
-
- for (i = 0; i < numslots; i++)
- {
- int sock = PQsocket(slots[i].connection);
-
- /*
- * We don't really expect any connections to lose their sockets
- * after startup, but just in case, cope by ignoring them.
- */
- if (sock < 0)
- continue;
-
- FD_SET(sock, &slotset);
- if (sock > maxFd)
- maxFd = sock;
- }
-
- SetCancelConn(slots->connection);
- i = select_loop(maxFd, &slotset, &aborting);
- ResetCancelConn();
-
- if (aborting)
- {
- /*
- * We set the cancel-receiving connection to the one in the zeroth
- * slot above, so fetch the error from there.
- */
- GetQueryResult(slots->connection, progname);
- return NULL;
- }
- Assert(i != 0);
-
- for (i = 0; i < numslots; i++)
- {
- int sock = PQsocket(slots[i].connection);
-
- if (sock >= 0 && FD_ISSET(sock, &slotset))
- {
- /* select() says input is available, so consume it */
- PQconsumeInput(slots[i].connection);
- }
-
- /* Collect result(s) as long as any are available */
- while (!PQisBusy(slots[i].connection))
- {
- PGresult *result = PQgetResult(slots[i].connection);
-
- if (result != NULL)
- {
- /* Check and discard the command result */
- if (!ProcessQueryResult(slots[i].connection, result,
- progname))
- return NULL;
- }
- else
- {
- /* This connection has become idle */
- slots[i].isFree = true;
- if (firstFree < 0)
- firstFree = i;
- break;
- }
- }
- }
- }
-
- return slots + firstFree;
-}
-
-/*
- * ProcessQueryResult
- *
- * Process (and delete) a query result. Returns true if there's no error,
- * false otherwise -- but errors about trying to vacuum a missing relation
- * are reported and subsequently ignored.
- */
-static bool
-ProcessQueryResult(PGconn *conn, PGresult *result, const char *progname)
-{
- /*
- * If it's an error, report it. Errors about a missing table are harmless
- * so we continue processing; but die for other errors.
- */
- if (PQresultStatus(result) != PGRES_COMMAND_OK)
- {
- char *sqlState = PQresultErrorField(result, PG_DIAG_SQLSTATE);
-
- pg_log_error("vacuuming of database \"%s\" failed: %s",
- PQdb(conn), PQerrorMessage(conn));
-
- if (sqlState && strcmp(sqlState, ERRCODE_UNDEFINED_TABLE) != 0)
- {
- PQclear(result);
- return false;
- }
- }
-
- PQclear(result);
- return true;
-}
-
-/*
- * GetQueryResult
- *
- * Pump the conn till it's dry of results; return false if any are errors.
- * Note that this will block if the conn is busy.
- */
-static bool
-GetQueryResult(PGconn *conn, const char *progname)
-{
- bool ok = true;
- PGresult *result;
-
- SetCancelConn(conn);
- while ((result = PQgetResult(conn)) != NULL)
- {
- if (!ProcessQueryResult(conn, result, progname))
- ok = false;
- }
- ResetCancelConn();
- return ok;
-}
-
-/*
- * DisconnectDatabase
- * Disconnect the connection associated with the given slot
- */
-static void
-DisconnectDatabase(ParallelSlot *slot)
-{
- char errbuf[256];
-
- if (!slot->connection)
- return;
-
- if (PQtransactionStatus(slot->connection) == PQTRANS_ACTIVE)
- {
- PGcancel *cancel;
-
- if ((cancel = PQgetCancel(slot->connection)))
- {
- (void) PQcancel(cancel, errbuf, sizeof(errbuf));
- PQfreeCancel(cancel);
- }
- }
-
- PQfinish(slot->connection);
- slot->connection = NULL;
-}
-
-/*
- * Loop on select() until a descriptor from the given set becomes readable.
- *
- * If we get a cancel request while we're waiting, we forego all further
- * processing and set the *aborting flag to true. The return value must be
- * ignored in this case. Otherwise, *aborting is set to false.
- */
-static int
-select_loop(int maxFd, fd_set *workerset, bool *aborting)
-{
- int i;
- fd_set saveSet = *workerset;
-
- if (CancelRequested)
- {
- *aborting = true;
- return -1;
- }
- else
- *aborting = false;
-
- for (;;)
- {
- /*
- * On Windows, we need to check once in a while for cancel requests;
- * on other platforms we rely on select() returning when interrupted.
- */
- struct timeval *tvp;
-#ifdef WIN32
- struct timeval tv = {0, 1000000};
-
- tvp = &tv;
-#else
- tvp = NULL;
-#endif
-
- *workerset = saveSet;
- i = select(maxFd + 1, workerset, NULL, NULL, tvp);
-
-#ifdef WIN32
- if (i == SOCKET_ERROR)
- {
- i = -1;
-
- if (WSAGetLastError() == WSAEINTR)
- errno = EINTR;
- }
-#endif
-
- if (i < 0 && errno == EINTR)
- continue; /* ignore this */
- if (i < 0 || CancelRequested)
- *aborting = true; /* but not this */
- if (i == 0)
- continue; /* timeout (Win32 only) */
- break;
- }
-
- return i;
-}
-
-static void
-init_slot(ParallelSlot *slot, PGconn *conn)
-{
- slot->connection = conn;
- /* Initially assume connection is idle */
- slot->isFree = true;
-}
-
static void
help(const char *progname)
{
--
2.20.1
0002-Add-parallel-processing-to-reindexdb.patchapplication/octet-stream; name=0002-Add-parallel-processing-to-reindexdb.patchDownload
From 877b84e20557be33c81a84c919b3110ef4c2ee60 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 28 Jun 2019 13:21:58 +0200
Subject: [PATCH 2/2] Add parallel processing to reindexdb
---
src/bin/scripts/Makefile | 2 +-
src/bin/scripts/reindexdb.c | 338 +++++++++++++++++++++++++----
src/bin/scripts/t/090_reindexdb.pl | 14 +-
3 files changed, 304 insertions(+), 50 deletions(-)
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 3cd793b134..ede665090f 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -29,7 +29,7 @@ dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+reindexdb: reindexdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
install: all installdirs
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index ca61348a0e..3f156112bf 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -10,10 +10,14 @@
*/
#include "postgres_fe.h"
+
+#include "catalog/pg_class_d.h"
#include "common.h"
#include "common/logging.h"
+#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
typedef enum ReindexType
{
@@ -25,16 +29,25 @@ typedef enum ReindexType
} ReindexType;
-static void reindex_one_database(const char *name, const char *dbname,
- ReindexType type, const char *host,
+static SimpleStringList *get_parallel_object_list(PGconn *conn,
+ const char *progname,
+ bool echo);
+static void reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
const char *port, const char *username,
enum trivalue prompt_password, const char *progname,
- bool echo, bool verbose, bool concurrently);
+ bool echo, bool verbose, bool concurrently,
+ int concurrentCons);
static void reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo,
- bool quiet, bool verbose, bool concurrently);
+ bool quiet, bool verbose, bool concurrently,
+ int concurrentCons);
+static void run_reindex_command(PGconn *conn, ReindexType type,
+ const char *name, const char *progname, bool echo,
+ bool verbose, bool concurrently, bool async);
+
static void help(const char *progname);
int
@@ -54,6 +67,7 @@ main(int argc, char *argv[])
{"system", no_argument, NULL, 's'},
{"table", required_argument, NULL, 't'},
{"index", required_argument, NULL, 'i'},
+ {"jobs", required_argument, NULL, 'j'},
{"verbose", no_argument, NULL, 'v'},
{"concurrently", no_argument, NULL, 1},
{"maintenance-db", required_argument, NULL, 2},
@@ -79,6 +93,9 @@ main(int argc, char *argv[])
SimpleStringList indexes = {NULL, NULL};
SimpleStringList tables = {NULL, NULL};
SimpleStringList schemas = {NULL, NULL};
+ int concurrentCons = 1;
+ int tbl_count = 0,
+ nsp_count = 0;
pg_logging_init(argv[0]);
progname = get_progname(argv[0]);
@@ -87,7 +104,7 @@ main(int argc, char *argv[])
handle_help_version_opts(argc, argv, "reindexdb", help);
/* process command-line options */
- while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:v", long_options, &optindex)) != -1)
+ while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:j:v", long_options, &optindex)) != -1)
{
switch (c)
{
@@ -114,6 +131,7 @@ main(int argc, char *argv[])
break;
case 'S':
simple_string_list_append(&schemas, optarg);
+ nsp_count++;
break;
case 'd':
dbname = pg_strdup(optarg);
@@ -126,10 +144,25 @@ main(int argc, char *argv[])
break;
case 't':
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
case 'i':
simple_string_list_append(&indexes, optarg);
break;
+ case 'j':
+ concurrentCons = atoi(optarg);
+ if (concurrentCons <= 0)
+ {
+ pg_log_error("number of parallel jobs must be at least 1");
+ exit(1);
+ }
+ if (concurrentCons > FD_SETSIZE - 1)
+ {
+ pg_log_error("too many parallel jobs requested (maximum: %d)",
+ FD_SETSIZE - 1);
+ exit(1);
+ }
+ break;
case 'v':
verbose = true;
break;
@@ -194,7 +227,8 @@ main(int argc, char *argv[])
}
reindex_all_databases(maintenance_db, host, port, username,
- prompt_password, progname, echo, quiet, verbose, concurrently);
+ prompt_password, progname, echo, quiet, verbose,
+ concurrently, 1);
}
else if (syscatalog)
{
@@ -214,6 +248,12 @@ main(int argc, char *argv[])
exit(1);
}
+ if (concurrentCons > 1)
+ {
+ pg_log_error("cannot use multiple jobs to reindex system catalogs");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -224,9 +264,9 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- reindex_one_database(NULL, dbname, REINDEX_SYSTEM, host,
+ reindex_one_database(dbname, REINDEX_SYSTEM, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, 1);
}
else
{
@@ -240,62 +280,60 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- if (schemas.head != NULL)
+ if (indexes.head != NULL)
{
- SimpleStringListCell *cell;
-
- for (cell = schemas.head; cell; cell = cell->next)
+ if (concurrentCons > 1)
{
- reindex_one_database(cell->val, dbname, REINDEX_SCHEMA, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ pg_log_error("cannot use multiple jobs to reindex multiple indexes");
+ exit(1);
}
+
+ reindex_one_database(dbname, REINDEX_INDEX, &indexes, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ 1);
}
- if (indexes.head != NULL)
- {
- SimpleStringListCell *cell;
+ if (schemas.head != NULL)
+ reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, nsp_count));
- for (cell = indexes.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_INDEX, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
if (tables.head != NULL)
- {
- SimpleStringListCell *cell;
-
- for (cell = tables.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_TABLE, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ reindex_one_database(dbname, REINDEX_TABLE, &tables, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, tbl_count));
/*
* reindex database only if neither index nor table nor schema is
* specified
*/
if (indexes.head == NULL && tables.head == NULL && schemas.head == NULL)
- reindex_one_database(NULL, dbname, REINDEX_DATABASE, host,
+ reindex_one_database(dbname, REINDEX_DATABASE, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, concurrentCons);
}
exit(0);
}
static void
-reindex_one_database(const char *name, const char *dbname, ReindexType type,
- const char *host, const char *port, const char *username,
+reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
+ const char *port, const char *username,
enum trivalue prompt_password, const char *progname, bool echo,
- bool verbose, bool concurrently)
+ bool verbose, bool concurrently, int concurrentCons)
{
- PQExpBufferData sql;
PGconn *conn;
+ SimpleStringListCell *cell;
+ bool parallel = concurrentCons > 1;
+ SimpleStringList *process_list = user_list;
+ ReindexType process_type = type;
+ ParallelSlot *slots;
+ int i;
+ bool failed = false;
conn = connectDatabase(dbname, host, port, username, prompt_password,
progname, echo, false, false);
@@ -308,6 +346,128 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
exit(1);
}
+ if (!parallel)
+ {
+ if (user_list == NULL)
+ {
+ /*
+ * Create a dummy list with an empty string, as user requires an
+ * element.
+ */
+ process_list = pg_malloc0(sizeof(SimpleStringList));
+ simple_string_list_append(process_list, "");
+ }
+ }
+ else
+ {
+ /*
+ * Database-wide parallel reindex requires special processing. If
+ * multiple jobs were asked, we have to reindex system catalogs first,
+ * as they can't be processed in parallel.
+ */
+ if (process_type == REINDEX_DATABASE)
+ {
+ Assert(user_list == NULL);
+ run_reindex_command(conn, REINDEX_SYSTEM, NULL, progname, echo,
+ verbose, concurrently, false);
+
+ process_type = REINDEX_TABLE;
+ process_list = get_parallel_object_list(conn, progname, echo);
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ PQfinish(conn);
+ }
+ else
+ Assert(user_list != NULL);
+ }
+
+ slots = (ParallelSlot *) pg_malloc(sizeof(ParallelSlot) * concurrentCons);
+ init_slot(slots, conn);
+ if (parallel)
+ {
+ for (i = 1; i < concurrentCons; i++)
+ {
+ conn = connectDatabase(dbname, host, port, username, prompt_password,
+ progname, echo, false, true);
+ init_slot(slots + i, conn);
+ }
+ }
+
+ cell = process_list->head;
+ do
+ {
+ const char *objname = cell->val;
+ ParallelSlot *free_slot = NULL;
+
+ if (CancelRequested)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ /*
+ * Get the connection slot to use. If in parallel mode, here we wait
+ * for one connection to become available if none already is. In
+ * non-parallel mode we simply use the only slot we have, which we
+ * know to be free.
+ */
+ if (parallel)
+ {
+ /*
+ * Get a free slot, waiting until one becomes free if none
+ * currently is.
+ */
+ free_slot = GetIdleSlot(slots, concurrentCons, progname);
+ if (!free_slot)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ free_slot->isFree = false;
+ }
+ else
+ free_slot = slots;
+
+ run_reindex_command(conn, process_type, objname, progname, echo,
+ verbose, concurrently, parallel);
+
+ cell = cell->next;
+ } while (cell != NULL);
+
+ if (parallel)
+ {
+ int j;
+
+ /* wait for all connections to finish */
+ for (j = 0; j < concurrentCons; j++)
+ {
+ if (!GetQueryResult((slots + j)->connection, progname))
+ {
+ failed = true;
+ goto finish;
+ }
+ }
+ }
+
+finish:
+ for (i = 0; i < concurrentCons; i++)
+ DisconnectDatabase(slots + i);
+ pfree(slots);
+
+ if (failed)
+ exit(1);
+}
+
+static void
+run_reindex_command(PGconn *conn, ReindexType type, const char *name,
+ const char *progname, bool echo, bool verbose,
+ bool concurrently, bool async)
+{
+ PQExpBufferData sql;
+ bool status;
+
/* build the REINDEX query */
initPQExpBuffer(&sql);
@@ -358,7 +518,17 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
/* finish the query */
appendPQExpBufferChar(&sql, ';');
- if (!executeMaintenanceCommand(conn, sql.data, echo))
+ if (async)
+ {
+ if (echo)
+ printf("%s\n", sql.data);
+
+ status = PQsendQuery(conn, sql.data) == 1;
+ }
+ else
+ status = executeMaintenanceCommand(conn, sql.data, echo);
+
+ if (!status)
{
switch (type)
{
@@ -383,20 +553,90 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
name, PQdb(conn), PQerrorMessage(conn));
break;
}
- PQfinish(conn);
- exit(1);
+ if (!async)
+ {
+ PQfinish(conn);
+ exit(1);
+ }
}
- PQfinish(conn);
termPQExpBuffer(&sql);
}
+/*
+ * Prepare the list of objects to process by querying the catalogs.
+ *
+ * This function will return a SimpleStringList object containing the entire
+ * list of tables in the given database that should be processed by a parallel
+ * database-wide reindex (excluding system tables), or NULL if there's no such
+ * table.
+ */
+static SimpleStringList *
+get_parallel_object_list(PGconn *conn, const char *progname, bool echo)
+{
+ PQExpBufferData catalog_query;
+ PQExpBufferData buf;
+ PGresult *res;
+ SimpleStringList *tables;
+ int ntups,
+ i;
+
+ initPQExpBuffer(&catalog_query);
+
+ /*
+ * This query is run using a safe search_path, so there's no need to fully
+ * qualify everything.
+ */
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE ns.nspname != 'pg_catalog'\n"
+ " AND c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " ORDER BY c.relpages DESC;");
+
+ res = executeQuery(conn, catalog_query.data, progname, echo);
+ termPQExpBuffer(&catalog_query);
+
+ /*
+ * If no rows are returned, there are no matching tables, so we are done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return NULL;
+ }
+
+ tables = pg_malloc0(sizeof(SimpleStringList));
+
+ /* Build qualified identifiers for each table */
+ initPQExpBuffer(&buf);
+ for (i = 0; i < ntups; i++)
+ {
+ appendPQExpBufferStr(&buf,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+
+ simple_string_list_append(tables, buf.data);
+ resetPQExpBuffer(&buf);
+ }
+ termPQExpBuffer(&buf);
+ PQclear(res);
+
+ return tables;
+}
+
static void
reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo, bool quiet, bool verbose,
- bool concurrently)
+ bool concurrently, int concurrentCons)
{
PGconn *conn;
PGresult *result;
@@ -423,9 +663,10 @@ reindex_all_databases(const char *maintenance_db,
appendPQExpBufferStr(&connstr, "dbname=");
appendConnStrVal(&connstr, dbname);
- reindex_one_database(NULL, connstr.data, REINDEX_DATABASE, host,
+ reindex_one_database(connstr.data, REINDEX_DATABASE, NULL, host,
port, username, prompt_password,
- progname, echo, verbose, concurrently);
+ progname, echo, verbose, concurrently,
+ concurrentCons);
}
termPQExpBuffer(&connstr);
@@ -444,6 +685,7 @@ help(const char *progname)
printf(_(" -d, --dbname=DBNAME database to reindex\n"));
printf(_(" -e, --echo show the commands being sent to the server\n"));
printf(_(" -i, --index=INDEX recreate specific index(es) only\n"));
+ printf(_(" -j, --jobs=NUM use this many concurrent connections to reindex\n"));
printf(_(" -q, --quiet don't write any messages\n"));
printf(_(" -s, --system reindex system catalogs\n"));
printf(_(" -S, --schema=SCHEMA reindex specific schema(s) only\n"));
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index 1af8ab70ad..7f52e50566 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -3,7 +3,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 34;
+use Test::More tests => 40;
program_help_ok('reindexdb');
program_version_ok('reindexdb');
@@ -77,3 +77,15 @@ $node->command_ok(
$node->command_ok(
[qw(reindexdb --echo --system dbname=template1)],
'reindexdb system with connection string');
+
+# parallel processing
+$node->command_fails([qw(reindexdb -j2 -s)],
+ 'reindexdb cannot process system catalogs in parallel');
+$node->command_fails([qw(reindexdb -j2 -i i1 -i i2)],
+ 'reindexdb cannot process indexes in parallel');
+$node->issues_sql_like([qw(reindexdb -j2)],
+ qr/statement: REINDEX SYSTEM postgres/,
+ 'Global and parallel reindex will issue a REINDEX SYSTEM');
+$node->issues_sql_like([qw(reindexdb -j2)],
+ qr/statement: REINDEX TABLE public.test1/,
+ 'Global and parallel reindex will issue per-table REINDEX');
--
2.20.1
On Mon, Jul 1, 2019 at 09:51:12AM -0400, Alvaro Herrera wrote:
Now that we have REINDEX CONCURRENTLY, I think reindexdb is going to
gain more popularity.Please don't reuse a file name as generic as "parallel.c" -- it's
annoying when navigating source. Maybe conn_parallel.c multiconn.c
connscripts.c admconnection.c ...?If your server crashes or is stopped midway during the reindex, you
would have to start again from scratch, and it's tedious (if it's
possible at all) to determine which indexes were missed. I think it
would be useful to have a two-phase mode: in the initial phase reindexdb
computes the list of indexes to be reindexed and saves them into a work
table somewhere. In the second phase, it reads indexes from that table
and processes them, marking them as done in the work table. If the
second phase crashes or is stopped, it can be restarted and consults the
work table. I would keep the work table, as it provides a bit of an
audit trail. It may be important to be able to run even if unable to
create such a work table (because of the <ironic>numerous</> users that
DROP DATABASE postgres).Maybe we'd have two flags in the work table for each index:
"reindex requested", "reindex done".
I think we have a similar issue with adding checksums, so let's address
with a generic framework and use it for all cases, like vacuumdb too.
--
Bruce Momjian <bruce@momjian.us> http://momjian.us
EnterpriseDB http://enterprisedb.com
+ As you are, so once was I. As I am, so you will be. +
+ Ancient Roman grave inscription +
On Mon, Jul 08, 2019 at 11:02:14PM +0200, Julien Rouhaud wrote:
On Mon, Jul 8, 2019 at 9:08 PM Julien Rouhaud <rjuju123@gmail.com> wrote:
I'll resubmit the parallel patch using per-table only
approachAttached.
I have done a lookup of this patch set with a focus on the refactoring
part, and the split is a bit confusing.
+void
+DisconnectDatabase(ParallelSlot *slot)
+{
+ char errbuf[256];
common.c has already an API to connect to a database. It would be
more natural to move the disconnection part also to common.c and have
the caller of DisconnectDatabase reset the slot connection by itself?
disconnectDatabase() (lower case for the first character) would make
the set more consistent. We could also have a wrapper like say
DiscardSlot() which does this work, but that seems like an overkill
for a single slot if one API could do the cleanup of the full set.
$ git grep select_loop
scripts_parallel.c: /* We must reconstruct the fd_set for each
call to select_loop */
scripts_parallel.c: i = select_loop(maxFd, &slotset, &aborting);
scripts_parallel.c:select_loop(int maxFd, fd_set *workerset, bool
*aborting)
scripts_parallel.h:extern int select_loop(int maxFd, fd_set
*workerset, bool *aborting);
select_loop is only present in scripts_parallel.c, so it can remain
static.
+ slots = (ParallelSlot *) pg_malloc(sizeof(ParallelSlot) *
concurrentCons);
+ init_slot(slots, conn);
+ if (parallel)
+ {
+ for (i = 1; i < concurrentCons; i++)
+ {
+ conn = connectDatabase(dbname, host, port,
username, prompt_password,
+
progname, echo, false, true);
+ init_slot(slots + i, conn);
+ }
+ }
This comes from 0002 and could be more refactored as vacuumdb does the
same thing. Based on 0001, init_slot() is called now in vacuumdb.c
and initializes a set of slots while connecting to a given database.
In short, in input we have a set of parameters and the ask to open
connections with N slots, and the return result is an pg_malloc'd
array of slots ready to be used. We could just call that
ParallelSlotInit() (if you have a better name feel free).
+ /*
+ * Get the connection slot to use. If in parallel mode, here we wait
+ * for one connection to become available if none already is. In
+ * non-parallel mode we simply use the only slot we have, which we
+ * know to be free.
+ */
+ if (parallel)
This business also is duplicated in both reindexdb.c and vacuumdb.c.
+bool
+GetQueryResult(PGconn *conn, const char *progname)
+{
This also does not stick with the parallel stuff, as that's basically
only getting a query result. We could stick that into common.c.
Patch 2 has no documentation. The option as well as the restrictions
in place need to be documented properly.
Here is a small idea about the set of routines we could have for the
parallel stuff, with only three of them needed to work on the parallel
slots and get free connections:
- Initialization of the full slot set.
- Cleanup and disconnection of the slots.
- Fetch an idle connection and wait for one until available.
--
Michael
Thanks for the review.
On Tue, Jul 9, 2019 at 9:24 AM Michael Paquier <michael@paquier.xyz> wrote:
On Mon, Jul 08, 2019 at 11:02:14PM +0200, Julien Rouhaud wrote:
On Mon, Jul 8, 2019 at 9:08 PM Julien Rouhaud <rjuju123@gmail.com> wrote:
I'll resubmit the parallel patch using per-table only
approachAttached.
I have done a lookup of this patch set with a focus on the refactoring
part, and the split is a bit confusing.
Yes, that wasn't a smart split :(
+void +DisconnectDatabase(ParallelSlot *slot) +{ + char errbuf[256]; common.c has already an API to connect to a database. It would be more natural to move the disconnection part also to common.c and have the caller of DisconnectDatabase reset the slot connection by itself?
Ok.
$ git grep select_loop
scripts_parallel.c: /* We must reconstruct the fd_set for each
call to select_loop */
scripts_parallel.c: i = select_loop(maxFd, &slotset, &aborting);
scripts_parallel.c:select_loop(int maxFd, fd_set *workerset, bool
*aborting)
scripts_parallel.h:extern int select_loop(int maxFd, fd_set
*workerset, bool *aborting);select_loop is only present in scripts_parallel.c, so it can remain
static.
Good point.
+ slots = (ParallelSlot *) pg_malloc(sizeof(ParallelSlot) * concurrentCons); + init_slot(slots, conn); + if (parallel) + { + for (i = 1; i < concurrentCons; i++) + { + conn = connectDatabase(dbname, host, port, username, prompt_password, + progname, echo, false, true); + init_slot(slots + i, conn); + } + }This comes from 0002 and could be more refactored as vacuumdb does the
same thing. Based on 0001, init_slot() is called now in vacuumdb.c
and initializes a set of slots while connecting to a given database.
In short, in input we have a set of parameters and the ask to open
connections with N slots, and the return result is an pg_malloc'd
array of slots ready to be used. We could just call that
ParallelSlotInit() (if you have a better name feel free).
Given how the rest of the functions are named, I'll probably use
InitParallelSlots().
+ /* + * Get the connection slot to use. If in parallel mode, here we wait + * for one connection to become available if none already is. In + * non-parallel mode we simply use the only slot we have, which we + * know to be free. + */ + if (parallel) This business also is duplicated in both reindexdb.c and vacuumdb.c.+bool +GetQueryResult(PGconn *conn, const char *progname) +{ This also does not stick with the parallel stuff, as that's basically only getting a query result. We could stick that into common.c.
This function also has a bad name, as it's discarding the result via
ProcessQueryResult. Maybe we should rename them to GetQuerySuccess()
and ConsumeAndTrashQueryResult()?
Patch 2 has no documentation. The option as well as the restrictions
in place need to be documented properly.
I forgot that I had forgotten to add documentation :( will fix this time.
On 2019-07-08 21:08, Julien Rouhaud wrote:
Don't get me wrong, I do agree that implementing filtering in the
backend is a better design. What's bothering me is that I also agree
that there will be more glibc breakage, and if that happens within a
few years, a lot of people will still be using pg12- version, and they
still won't have an efficient way to rebuild their indexes. Now, it'd
be easy to publish an external tools that does a simple
parallel-and-glic-filtering reindex tool that will serve that purpose
for the few years it'll be needed, so everyone can be happy.
You can already do that: Run a query through psql to get a list of
affected tables or indexes and feed those to reindexdb using -i or -t
options.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Tue, Jul 9, 2019 at 9:52 AM Julien Rouhaud <rjuju123@gmail.com> wrote:
On Tue, Jul 9, 2019 at 9:24 AM Michael Paquier <michael@paquier.xyz> wrote:
I have done a lookup of this patch set with a focus on the refactoring
part, and the split is a bit confusing.[...]
I finished to do a better refactoring, and ended up with this API in
scripts_parallel:
extern ParallelSlot *ConsumeIdleSlot(ParallelSlot *slots, int numslots,
const char *progname);
extern ParallelSlot *SetupParallelSlots(const char *dbname, const char *host,
const char *port,
const char *username, bool prompt_password,
const char *progname, bool echo,
PGconn *conn, int numslots);
extern bool WaitForSlotsCompletion(ParallelSlot *slots, int numslots,
const char *progname);
ConsumeIdleSlot() being a wrapper on top of (now static) GetIdleSlot,
which handles parallelism and possible failure.
Attached v3, including updated documentation for the new -j option.
Attachments:
0002-Add-parallel-processing-to-reindexdb-v3.patchapplication/octet-stream; name=0002-Add-parallel-processing-to-reindexdb-v3.patchDownload
From a47c0da05a8ffc9119ad5f939c0e0d2956b8b506 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 28 Jun 2019 13:21:58 +0200
Subject: [PATCH 2/2] Add parallel processing to reindexdb
---
doc/src/sgml/ref/reindexdb.sgml | 23 +++
src/bin/scripts/Makefile | 2 +-
src/bin/scripts/reindexdb.c | 311 ++++++++++++++++++++++++-----
src/bin/scripts/t/090_reindexdb.pl | 14 +-
4 files changed, 300 insertions(+), 50 deletions(-)
diff --git a/doc/src/sgml/ref/reindexdb.sgml b/doc/src/sgml/ref/reindexdb.sgml
index 25b5a72770..a7031030b9 100644
--- a/doc/src/sgml/ref/reindexdb.sgml
+++ b/doc/src/sgml/ref/reindexdb.sgml
@@ -166,6 +166,29 @@ PostgreSQL documentation
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>-j <replaceable class="parameter">njobs</replaceable></option></term>
+ <term><option>--jobs=<replaceable class="parameter">njobs</replaceable></option></term>
+ <listitem>
+ <para>
+ Execute the reindex commands in parallel by running
+ <replaceable class="parameter">njobs</replaceable>
+ commands simultaneously. This option reduces the time of the
+ processing but it also increases the load on the database server.
+ </para>
+ <para>
+ <application>reindexdb</application> will open
+ <replaceable class="parameter">njobs</replaceable> connections to the
+ database, so make sure your <xref linkend="guc-max-connections"/>
+ setting is high enough to accommodate all connections.
+ </para>
+ <para>
+ Note that this mode is not compatible the <option>-i / --index</option>
+ or the <option>-s / --system</option> options.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>-q</option></term>
<term><option>--quiet</option></term>
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 3cd793b134..ede665090f 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -29,7 +29,7 @@ dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+reindexdb: reindexdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
install: all installdirs
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index ca61348a0e..45b1f5b4c8 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -10,10 +10,14 @@
*/
#include "postgres_fe.h"
+
+#include "catalog/pg_class_d.h"
#include "common.h"
#include "common/logging.h"
+#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
typedef enum ReindexType
{
@@ -25,16 +29,25 @@ typedef enum ReindexType
} ReindexType;
-static void reindex_one_database(const char *name, const char *dbname,
- ReindexType type, const char *host,
+static SimpleStringList *get_parallel_object_list(PGconn *conn,
+ const char *progname,
+ bool echo);
+static void reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
const char *port, const char *username,
enum trivalue prompt_password, const char *progname,
- bool echo, bool verbose, bool concurrently);
+ bool echo, bool verbose, bool concurrently,
+ int concurrentCons);
static void reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo,
- bool quiet, bool verbose, bool concurrently);
+ bool quiet, bool verbose, bool concurrently,
+ int concurrentCons);
+static void run_reindex_command(PGconn *conn, ReindexType type,
+ const char *name, const char *progname, bool echo,
+ bool verbose, bool concurrently, bool async);
+
static void help(const char *progname);
int
@@ -54,6 +67,7 @@ main(int argc, char *argv[])
{"system", no_argument, NULL, 's'},
{"table", required_argument, NULL, 't'},
{"index", required_argument, NULL, 'i'},
+ {"jobs", required_argument, NULL, 'j'},
{"verbose", no_argument, NULL, 'v'},
{"concurrently", no_argument, NULL, 1},
{"maintenance-db", required_argument, NULL, 2},
@@ -79,6 +93,9 @@ main(int argc, char *argv[])
SimpleStringList indexes = {NULL, NULL};
SimpleStringList tables = {NULL, NULL};
SimpleStringList schemas = {NULL, NULL};
+ int concurrentCons = 1;
+ int tbl_count = 0,
+ nsp_count = 0;
pg_logging_init(argv[0]);
progname = get_progname(argv[0]);
@@ -87,7 +104,7 @@ main(int argc, char *argv[])
handle_help_version_opts(argc, argv, "reindexdb", help);
/* process command-line options */
- while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:v", long_options, &optindex)) != -1)
+ while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:j:v", long_options, &optindex)) != -1)
{
switch (c)
{
@@ -114,6 +131,7 @@ main(int argc, char *argv[])
break;
case 'S':
simple_string_list_append(&schemas, optarg);
+ nsp_count++;
break;
case 'd':
dbname = pg_strdup(optarg);
@@ -126,10 +144,25 @@ main(int argc, char *argv[])
break;
case 't':
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
case 'i':
simple_string_list_append(&indexes, optarg);
break;
+ case 'j':
+ concurrentCons = atoi(optarg);
+ if (concurrentCons <= 0)
+ {
+ pg_log_error("number of parallel jobs must be at least 1");
+ exit(1);
+ }
+ if (concurrentCons > FD_SETSIZE - 1)
+ {
+ pg_log_error("too many parallel jobs requested (maximum: %d)",
+ FD_SETSIZE - 1);
+ exit(1);
+ }
+ break;
case 'v':
verbose = true;
break;
@@ -194,7 +227,8 @@ main(int argc, char *argv[])
}
reindex_all_databases(maintenance_db, host, port, username,
- prompt_password, progname, echo, quiet, verbose, concurrently);
+ prompt_password, progname, echo, quiet, verbose,
+ concurrently, 1);
}
else if (syscatalog)
{
@@ -214,6 +248,12 @@ main(int argc, char *argv[])
exit(1);
}
+ if (concurrentCons > 1)
+ {
+ pg_log_error("cannot use multiple jobs to reindex system catalogs");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -224,9 +264,9 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- reindex_one_database(NULL, dbname, REINDEX_SYSTEM, host,
+ reindex_one_database(dbname, REINDEX_SYSTEM, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, 1);
}
else
{
@@ -240,62 +280,60 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- if (schemas.head != NULL)
+ if (indexes.head != NULL)
{
- SimpleStringListCell *cell;
-
- for (cell = schemas.head; cell; cell = cell->next)
+ if (concurrentCons > 1)
{
- reindex_one_database(cell->val, dbname, REINDEX_SCHEMA, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ pg_log_error("cannot use multiple jobs to reindex multiple indexes");
+ exit(1);
}
+
+ reindex_one_database(dbname, REINDEX_INDEX, &indexes, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ 1);
}
- if (indexes.head != NULL)
- {
- SimpleStringListCell *cell;
+ if (schemas.head != NULL)
+ reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, nsp_count));
- for (cell = indexes.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_INDEX, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
if (tables.head != NULL)
- {
- SimpleStringListCell *cell;
-
- for (cell = tables.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_TABLE, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ reindex_one_database(dbname, REINDEX_TABLE, &tables, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, tbl_count));
/*
* reindex database only if neither index nor table nor schema is
* specified
*/
if (indexes.head == NULL && tables.head == NULL && schemas.head == NULL)
- reindex_one_database(NULL, dbname, REINDEX_DATABASE, host,
+ reindex_one_database(dbname, REINDEX_DATABASE, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, concurrentCons);
}
exit(0);
}
static void
-reindex_one_database(const char *name, const char *dbname, ReindexType type,
- const char *host, const char *port, const char *username,
+reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
+ const char *port, const char *username,
enum trivalue prompt_password, const char *progname, bool echo,
- bool verbose, bool concurrently)
+ bool verbose, bool concurrently, int concurrentCons)
{
- PQExpBufferData sql;
PGconn *conn;
+ SimpleStringListCell *cell;
+ bool parallel = concurrentCons > 1;
+ SimpleStringList *process_list = user_list;
+ ReindexType process_type = type;
+ ParallelSlot *slots;
+ int i;
+ bool failed = false;
conn = connectDatabase(dbname, host, port, username, prompt_password,
progname, echo, false, false);
@@ -308,6 +346,101 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
exit(1);
}
+ if (!parallel)
+ {
+ if (user_list == NULL)
+ {
+ /*
+ * Create a dummy list with an empty string, as user requires an
+ * element.
+ */
+ process_list = pg_malloc0(sizeof(SimpleStringList));
+ simple_string_list_append(process_list, "");
+ }
+ }
+ else
+ {
+ /*
+ * Database-wide parallel reindex requires special processing. If
+ * multiple jobs were asked, we have to reindex system catalogs first,
+ * as they can't be processed in parallel.
+ */
+ if (process_type == REINDEX_DATABASE)
+ {
+ Assert(user_list == NULL);
+ run_reindex_command(conn, REINDEX_SYSTEM, NULL, progname, echo,
+ verbose, concurrently, false);
+
+ process_type = REINDEX_TABLE;
+ process_list = get_parallel_object_list(conn, progname, echo);
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ }
+ else
+ Assert(user_list != NULL);
+ }
+
+ slots = SetupParallelSlots(dbname, host, port, username, prompt_password,
+ progname, echo, conn, concurrentCons);
+
+ cell = process_list->head;
+ do
+ {
+ const char *objname = cell->val;
+ ParallelSlot *free_slot = NULL;
+
+ if (CancelRequested)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ free_slot = ConsumeIdleSlot(slots, concurrentCons, progname);
+ if (!free_slot)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ run_reindex_command(conn, process_type, objname, progname, echo,
+ verbose, concurrently, parallel);
+
+ cell = cell->next;
+ } while (cell != NULL);
+
+ if (!WaitForSlotsCompletion(slots, concurrentCons, progname))
+ failed = true;
+
+finish:
+ for (i = 0; i < concurrentCons; i++)
+ {
+ PGconn *conn = slots[i].connection;
+
+ if (conn == NULL)
+ continue;
+
+ disconnectDatabase(conn);
+ conn = NULL;
+ }
+ pfree(slots);
+
+ if (failed)
+ exit(1);
+}
+
+static void
+run_reindex_command(PGconn *conn, ReindexType type, const char *name,
+ const char *progname, bool echo, bool verbose,
+ bool concurrently, bool async)
+{
+ PQExpBufferData sql;
+ bool status;
+
/* build the REINDEX query */
initPQExpBuffer(&sql);
@@ -358,7 +491,17 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
/* finish the query */
appendPQExpBufferChar(&sql, ';');
- if (!executeMaintenanceCommand(conn, sql.data, echo))
+ if (async)
+ {
+ if (echo)
+ printf("%s\n", sql.data);
+
+ status = PQsendQuery(conn, sql.data) == 1;
+ }
+ else
+ status = executeMaintenanceCommand(conn, sql.data, echo);
+
+ if (!status)
{
switch (type)
{
@@ -383,20 +526,90 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
name, PQdb(conn), PQerrorMessage(conn));
break;
}
- PQfinish(conn);
- exit(1);
+ if (!async)
+ {
+ PQfinish(conn);
+ exit(1);
+ }
}
- PQfinish(conn);
termPQExpBuffer(&sql);
}
+/*
+ * Prepare the list of objects to process by querying the catalogs.
+ *
+ * This function will return a SimpleStringList object containing the entire
+ * list of tables in the given database that should be processed by a parallel
+ * database-wide reindex (excluding system tables), or NULL if there's no such
+ * table.
+ */
+static SimpleStringList *
+get_parallel_object_list(PGconn *conn, const char *progname, bool echo)
+{
+ PQExpBufferData catalog_query;
+ PQExpBufferData buf;
+ PGresult *res;
+ SimpleStringList *tables;
+ int ntups,
+ i;
+
+ initPQExpBuffer(&catalog_query);
+
+ /*
+ * This query is run using a safe search_path, so there's no need to fully
+ * qualify everything.
+ */
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE ns.nspname != 'pg_catalog'\n"
+ " AND c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " ORDER BY c.relpages DESC;");
+
+ res = executeQuery(conn, catalog_query.data, progname, echo);
+ termPQExpBuffer(&catalog_query);
+
+ /*
+ * If no rows are returned, there are no matching tables, so we are done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return NULL;
+ }
+
+ tables = pg_malloc0(sizeof(SimpleStringList));
+
+ /* Build qualified identifiers for each table */
+ initPQExpBuffer(&buf);
+ for (i = 0; i < ntups; i++)
+ {
+ appendPQExpBufferStr(&buf,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+
+ simple_string_list_append(tables, buf.data);
+ resetPQExpBuffer(&buf);
+ }
+ termPQExpBuffer(&buf);
+ PQclear(res);
+
+ return tables;
+}
+
static void
reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo, bool quiet, bool verbose,
- bool concurrently)
+ bool concurrently, int concurrentCons)
{
PGconn *conn;
PGresult *result;
@@ -423,9 +636,10 @@ reindex_all_databases(const char *maintenance_db,
appendPQExpBufferStr(&connstr, "dbname=");
appendConnStrVal(&connstr, dbname);
- reindex_one_database(NULL, connstr.data, REINDEX_DATABASE, host,
+ reindex_one_database(connstr.data, REINDEX_DATABASE, NULL, host,
port, username, prompt_password,
- progname, echo, verbose, concurrently);
+ progname, echo, verbose, concurrently,
+ concurrentCons);
}
termPQExpBuffer(&connstr);
@@ -444,6 +658,7 @@ help(const char *progname)
printf(_(" -d, --dbname=DBNAME database to reindex\n"));
printf(_(" -e, --echo show the commands being sent to the server\n"));
printf(_(" -i, --index=INDEX recreate specific index(es) only\n"));
+ printf(_(" -j, --jobs=NUM use this many concurrent connections to reindex\n"));
printf(_(" -q, --quiet don't write any messages\n"));
printf(_(" -s, --system reindex system catalogs\n"));
printf(_(" -S, --schema=SCHEMA reindex specific schema(s) only\n"));
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index 1af8ab70ad..7f52e50566 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -3,7 +3,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 34;
+use Test::More tests => 40;
program_help_ok('reindexdb');
program_version_ok('reindexdb');
@@ -77,3 +77,15 @@ $node->command_ok(
$node->command_ok(
[qw(reindexdb --echo --system dbname=template1)],
'reindexdb system with connection string');
+
+# parallel processing
+$node->command_fails([qw(reindexdb -j2 -s)],
+ 'reindexdb cannot process system catalogs in parallel');
+$node->command_fails([qw(reindexdb -j2 -i i1 -i i2)],
+ 'reindexdb cannot process indexes in parallel');
+$node->issues_sql_like([qw(reindexdb -j2)],
+ qr/statement: REINDEX SYSTEM postgres/,
+ 'Global and parallel reindex will issue a REINDEX SYSTEM');
+$node->issues_sql_like([qw(reindexdb -j2)],
+ qr/statement: REINDEX TABLE public.test1/,
+ 'Global and parallel reindex will issue per-table REINDEX');
--
2.20.1
0001-Export-vacuumdb-s-parallel-infrastructure-v3.patchapplication/octet-stream; name=0001-Export-vacuumdb-s-parallel-infrastructure-v3.patchDownload
From db00f7c7546724ef28cdaa8aafedec8491be6040 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 28 Jun 2019 13:11:29 +0200
Subject: [PATCH 1/2] Export vacuumdb's parallel infrastructure
---
src/bin/scripts/Makefile | 4 +-
src/bin/scripts/common.c | 78 +++++++
src/bin/scripts/common.h | 7 +
src/bin/scripts/scripts_parallel.c | 279 +++++++++++++++++++++++
src/bin/scripts/scripts_parallel.h | 32 +++
src/bin/scripts/vacuumdb.c | 349 ++---------------------------
6 files changed, 417 insertions(+), 332 deletions(-)
create mode 100644 src/bin/scripts/scripts_parallel.c
create mode 100644 src/bin/scripts/scripts_parallel.h
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 9f352b5e2b..3cd793b134 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -28,7 +28,7 @@ createuser: createuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport
dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-vacuumdb: vacuumdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
@@ -50,7 +50,7 @@ uninstall:
clean distclean maintainer-clean:
rm -f $(addsuffix $(X), $(PROGRAMS)) $(addsuffix .o, $(PROGRAMS))
- rm -f common.o $(WIN32RES)
+ rm -f common.o scripts_parallel.o $(WIN32RES)
rm -rf tmp_check
check:
diff --git a/src/bin/scripts/common.c b/src/bin/scripts/common.c
index 296029d809..7250b57bc4 100644
--- a/src/bin/scripts/common.c
+++ b/src/bin/scripts/common.c
@@ -22,6 +22,8 @@
#include "fe_utils/connect.h"
#include "fe_utils/string_utils.h"
+#define ERRCODE_UNDEFINED_TABLE "42P01"
+
static PGcancel *volatile cancelConn = NULL;
bool CancelRequested = false;
@@ -178,6 +180,28 @@ connectMaintenanceDatabase(const char *maintenance_db,
return conn;
}
+/*
+ * Disconnect the given connection, canceling any statement if one is active.
+ */
+void
+disconnectDatabase(PGconn *conn)
+{
+ char errbuf[256];
+
+ if (PQtransactionStatus(conn) == PQTRANS_ACTIVE)
+ {
+ PGcancel *cancel;
+
+ if ((cancel = PQgetCancel(conn)))
+ {
+ (void) PQcancel(cancel, errbuf, sizeof(errbuf));
+ PQfreeCancel(cancel);
+ }
+ }
+
+ PQfinish(conn);
+}
+
/*
* Run a query, return the results, exit program on failure.
*/
@@ -255,6 +279,60 @@ executeMaintenanceCommand(PGconn *conn, const char *query, bool echo)
return r;
}
+/*
+ * getQuerySucess
+ *
+ * Pump the conn till it's dry of results; return false if any are errors.
+ * Note that this will block if the conn is busy.
+ */
+bool
+getQuerySuccess(PGconn *conn, const char *progname)
+{
+ bool ok = true;
+ PGresult *result;
+
+ SetCancelConn(conn);
+ while ((result = PQgetResult(conn)) != NULL)
+ {
+ if (!processQueryResult(conn, result, progname))
+ ok = false;
+ }
+ ResetCancelConn();
+ return ok;
+}
+
+/*
+ * processQueryResult
+ *
+ * Process (and delete) a query result. Returns true if there's no error,
+ * false otherwise -- but errors about trying to vacuum a missing relation
+ * are reported and subsequently ignored.
+ */
+bool
+processQueryResult(PGconn *conn, PGresult *result, const char *progname)
+{
+ /*
+ * If it's an error, report it. Errors about a missing table are harmless
+ * so we continue processing; but die for other errors.
+ */
+ if (PQresultStatus(result) != PGRES_COMMAND_OK)
+ {
+ char *sqlState = PQresultErrorField(result, PG_DIAG_SQLSTATE);
+
+ pg_log_error("processing of database \"%s\" failed: %s",
+ PQdb(conn), PQerrorMessage(conn));
+
+ if (sqlState && strcmp(sqlState, ERRCODE_UNDEFINED_TABLE) != 0)
+ {
+ PQclear(result);
+ return false;
+ }
+ }
+
+ PQclear(result);
+ return true;
+}
+
/*
* Split TABLE[(COLUMNS)] into TABLE and [(COLUMNS)] portions. When you
diff --git a/src/bin/scripts/common.h b/src/bin/scripts/common.h
index 35d1a3e0d5..9f10e1da96 100644
--- a/src/bin/scripts/common.h
+++ b/src/bin/scripts/common.h
@@ -39,6 +39,8 @@ extern PGconn *connectMaintenanceDatabase(const char *maintenance_db,
const char *pguser, enum trivalue prompt_password,
const char *progname, bool echo);
+extern void disconnectDatabase(PGconn *conn);
+
extern PGresult *executeQuery(PGconn *conn, const char *query,
const char *progname, bool echo);
@@ -48,6 +50,11 @@ extern void executeCommand(PGconn *conn, const char *query,
extern bool executeMaintenanceCommand(PGconn *conn, const char *query,
bool echo);
+extern bool getQuerySuccess(PGconn *conn, const char *progname);
+
+extern bool processQueryResult(PGconn *conn, PGresult *result,
+ const char *progname);
+
extern void splitTableColumnsSpec(const char *spec, int encoding,
char **table, const char **columns);
diff --git a/src/bin/scripts/scripts_parallel.c b/src/bin/scripts/scripts_parallel.c
new file mode 100644
index 0000000000..d75f1e1960
--- /dev/null
+++ b/src/bin/scripts/scripts_parallel.c
@@ -0,0 +1,279 @@
+/*-------------------------------------------------------------------------
+ *
+ * scripts_parallel.c
+ * Parallel support for bin/scripts/
+ *
+ *
+ * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/bin/scripts/scripts_parallel.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#ifdef HAVE_SYS_SELECT_H
+#include <sys/select.h>
+#endif
+
+#include "common.h"
+#include "common/logging.h"
+#include "scripts_parallel.h"
+
+static ParallelSlot *get_idle_slot(ParallelSlot slots[], int numslots,
+ const char *progname);
+static void init_slot(ParallelSlot *slot, PGconn *conn);
+static int select_loop(int maxFd, fd_set *workerset, bool *aborting);
+
+
+/*
+ * Get the connection slot to use. If there are multiples slots, here we wait
+ * for one connection to become available if none already is, returning NULL if
+ * an error occured. Otherwise, we simply use the only slot we have, which we know to
+ * be free.
+ */
+ParallelSlot *
+ConsumeIdleSlot(ParallelSlot *slots, int numslots,
+ const char *progname)
+{
+ ParallelSlot *free_slot;
+
+ if (numslots > 1)
+ {
+ /*
+ * Get a free slot, waiting until one becomes free if none currently
+ * is.
+ */
+ free_slot = get_idle_slot(slots, numslots, progname);
+ if (!free_slot)
+ return NULL;
+
+ free_slot->isFree = false;
+ }
+ else
+ free_slot = slots;
+
+ return free_slot;
+}
+
+/*
+ * get_idle_slot
+ * Return a connection slot that is ready to execute a command.
+ *
+ * We return the first slot we find that is marked isFree, if one is;
+ * otherwise, we loop on select() until one socket becomes available. When
+ * this happens, we read the whole set and mark as free all sockets that become
+ * available.
+ *
+ * If an error occurs, NULL is returned.
+ */
+static ParallelSlot *
+get_idle_slot(ParallelSlot slots[], int numslots,
+ const char *progname)
+{
+ int i;
+ int firstFree = -1;
+
+ /* Any connection already known free? */
+ for (i = 0; i < numslots; i++)
+ {
+ if (slots[i].isFree)
+ return slots + i;
+ }
+
+ /*
+ * No free slot found, so wait until one of the connections has finished
+ * its task and return the available slot.
+ */
+ while (firstFree < 0)
+ {
+ fd_set slotset;
+ int maxFd = 0;
+ bool aborting;
+
+ /* We must reconstruct the fd_set for each call to select_loop */
+ FD_ZERO(&slotset);
+
+ for (i = 0; i < numslots; i++)
+ {
+ int sock = PQsocket(slots[i].connection);
+
+ /*
+ * We don't really expect any connections to lose their sockets
+ * after startup, but just in case, cope by ignoring them.
+ */
+ if (sock < 0)
+ continue;
+
+ FD_SET(sock, &slotset);
+ if (sock > maxFd)
+ maxFd = sock;
+ }
+
+ SetCancelConn(slots->connection);
+ i = select_loop(maxFd, &slotset, &aborting);
+ ResetCancelConn();
+
+ if (aborting)
+ {
+ /*
+ * We set the cancel-receiving connection to the one in the zeroth
+ * slot above, so fetch the error from there.
+ */
+ getQuerySuccess(slots->connection, progname);
+ return NULL;
+ }
+ Assert(i != 0);
+
+ for (i = 0; i < numslots; i++)
+ {
+ int sock = PQsocket(slots[i].connection);
+
+ if (sock >= 0 && FD_ISSET(sock, &slotset))
+ {
+ /* select() says input is available, so consume it */
+ PQconsumeInput(slots[i].connection);
+ }
+
+ /* Collect result(s) as long as any are available */
+ while (!PQisBusy(slots[i].connection))
+ {
+ PGresult *result = PQgetResult(slots[i].connection);
+
+ if (result != NULL)
+ {
+ /* Check and discard the command result */
+ if (!processQueryResult(slots[i].connection, result,
+ progname))
+ return NULL;
+ }
+ else
+ {
+ /* This connection has become idle */
+ slots[i].isFree = true;
+ if (firstFree < 0)
+ firstFree = i;
+ break;
+ }
+ }
+ }
+ }
+
+ return slots + firstFree;
+}
+
+static void
+init_slot(ParallelSlot *slot, PGconn *conn)
+{
+ slot->connection = conn;
+ /* Initially assume connection is idle */
+ slot->isFree = true;
+}
+
+ParallelSlot *
+SetupParallelSlots(const char *dbname, const char *host, const char *port,
+ const char *username, bool prompt_password, const char *progname,
+ bool echo, PGconn *conn, int numslots)
+{
+ ParallelSlot *slots;
+ int i;
+
+ slots = (ParallelSlot *) pg_malloc(sizeof(ParallelSlot) * numslots);
+ init_slot(slots, conn);
+ if (numslots > 1)
+ {
+ for (i = 1; i < numslots; i++)
+ {
+ conn = connectDatabase(dbname, host, port, username, prompt_password,
+ progname, echo, false, true);
+ init_slot(slots + i, conn);
+ }
+ }
+
+ return slots;
+}
+
+/*
+ * Loop on select() until a descriptor from the given set becomes readable.
+ *
+ * If we get a cancel request while we're waiting, we forego all further
+ * processing and set the *aborting flag to true. The return value must be
+ * ignored in this case. Otherwise, *aborting is set to false.
+ */
+static int
+select_loop(int maxFd, fd_set *workerset, bool *aborting)
+{
+ int i;
+ fd_set saveSet = *workerset;
+
+ if (CancelRequested)
+ {
+ *aborting = true;
+ return -1;
+ }
+ else
+ *aborting = false;
+
+ for (;;)
+ {
+ /*
+ * On Windows, we need to check once in a while for cancel requests;
+ * on other platforms we rely on select() returning when interrupted.
+ */
+ struct timeval *tvp;
+#ifdef WIN32
+ struct timeval tv = {0, 1000000};
+
+ tvp = &tv;
+#else
+ tvp = NULL;
+#endif
+
+ *workerset = saveSet;
+ i = select(maxFd + 1, workerset, NULL, NULL, tvp);
+
+#ifdef WIN32
+ if (i == SOCKET_ERROR)
+ {
+ i = -1;
+
+ if (WSAGetLastError() == WSAEINTR)
+ errno = EINTR;
+ }
+#endif
+
+ if (i < 0 && errno == EINTR)
+ continue; /* ignore this */
+ if (i < 0 || CancelRequested)
+ *aborting = true; /* but not this */
+ if (i == 0)
+ continue; /* timeout (Win32 only) */
+ break;
+ }
+
+ return i;
+}
+
+/* Wait for all connections to finish, returning true if no error occured.. */
+bool
+WaitForSlotsCompletion(ParallelSlot *slots, int numslots, const char *progname)
+{
+ int i;
+
+ /*
+ * If the callers didn't use multiple connections, it already waited for
+ * query completion
+ */
+ if (numslots <= 1)
+ return true;
+
+ for (i = 0; i < numslots; i++)
+ {
+ if (!getQuerySuccess((slots + i)->connection, progname))
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/bin/scripts/scripts_parallel.h b/src/bin/scripts/scripts_parallel.h
new file mode 100644
index 0000000000..34111fd310
--- /dev/null
+++ b/src/bin/scripts/scripts_parallel.h
@@ -0,0 +1,32 @@
+/*
+ * scripts_parallel.h
+ * Parallel support for bin/scripts/
+ *
+ * Copyright (c) 2003-2019, PostgreSQL Global Development Group
+ *
+ * src/bin/scripts/scripts_parallel.h
+ */
+#ifndef SCRIPTS_PARALLEL_H
+#define SCRIPTS_PARALLEL_H
+
+/* Parallel processing stuff */
+typedef struct ParallelSlot
+{
+ PGconn *connection; /* One connection */
+ bool isFree; /* Is it known to be idle? */
+} ParallelSlot;
+
+extern ParallelSlot *ConsumeIdleSlot(ParallelSlot *slots, int numslots,
+ const char *progname);
+
+extern ParallelSlot *SetupParallelSlots(const char *dbname, const char *host,
+ const char *port,
+ const char *username, bool prompt_password,
+ const char *progname, bool echo,
+ PGconn *conn, int numslots);
+
+extern bool WaitForSlotsCompletion(ParallelSlot *slots, int numslots,
+ const char *progname);
+
+
+#endif /* SCRIPTS_PARALLEL_H */
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 3bcd14b4dc..78e315eb44 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -12,10 +12,6 @@
#include "postgres_fe.h"
-#ifdef HAVE_SYS_SELECT_H
-#include <sys/select.h>
-#endif
-
#include "catalog/pg_class_d.h"
#include "common.h"
@@ -23,17 +19,9 @@
#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
-#define ERRCODE_UNDEFINED_TABLE "42P01"
-
-/* Parallel vacuuming stuff */
-typedef struct ParallelSlot
-{
- PGconn *connection; /* One connection */
- bool isFree; /* Is it known to be idle? */
-} ParallelSlot;
-
/* vacuum options controlled by user flags */
typedef struct vacuumingOptions
{
@@ -71,20 +59,6 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
const char *table, const char *progname, bool async);
-static ParallelSlot *GetIdleSlot(ParallelSlot slots[], int numslots,
- const char *progname);
-
-static bool ProcessQueryResult(PGconn *conn, PGresult *result,
- const char *progname);
-
-static bool GetQueryResult(PGconn *conn, const char *progname);
-
-static void DisconnectDatabase(ParallelSlot *slot);
-
-static int select_loop(int maxFd, fd_set *workerset, bool *aborting);
-
-static void init_slot(ParallelSlot *slot, PGconn *conn);
-
static void help(const char *progname);
/* For analyze-in-stages mode */
@@ -625,17 +599,9 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
*/
if (concurrentCons <= 0)
concurrentCons = 1;
- slots = (ParallelSlot *) pg_malloc(sizeof(ParallelSlot) * concurrentCons);
- init_slot(slots, conn);
- if (parallel)
- {
- for (i = 1; i < concurrentCons; i++)
- {
- conn = connectDatabase(dbname, host, port, username, prompt_password,
- progname, echo, false, true);
- init_slot(slots + i, conn);
- }
- }
+
+ slots = SetupParallelSlots(dbname, host, port, username, prompt_password,
+ progname, echo, conn, concurrentCons);
/*
* Prepare all the connections to run the appropriate analyze stage, if
@@ -666,29 +632,12 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
goto finish;
}
- /*
- * Get the connection slot to use. If in parallel mode, here we wait
- * for one connection to become available if none already is. In
- * non-parallel mode we simply use the only slot we have, which we
- * know to be free.
- */
- if (parallel)
+ free_slot = ConsumeIdleSlot(slots, concurrentCons, progname);
+ if (!free_slot)
{
- /*
- * Get a free slot, waiting until one becomes free if none
- * currently is.
- */
- free_slot = GetIdleSlot(slots, concurrentCons, progname);
- if (!free_slot)
- {
- failed = true;
- goto finish;
- }
-
- free_slot->isFree = false;
+ failed = true;
+ goto finish;
}
- else
- free_slot = slots;
prepare_vacuum_command(&sql, PQserverVersion(free_slot->connection),
vacopts, tabname);
@@ -704,24 +653,20 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
cell = cell->next;
} while (cell != NULL);
- if (parallel)
- {
- int j;
-
- /* wait for all connections to finish */
- for (j = 0; j < concurrentCons; j++)
- {
- if (!GetQueryResult((slots + j)->connection, progname))
- {
- failed = true;
- goto finish;
- }
- }
- }
+ if (!WaitForSlotsCompletion(slots, concurrentCons, progname))
+ failed = true;
finish:
for (i = 0; i < concurrentCons; i++)
- DisconnectDatabase(slots + i);
+ {
+ PGconn *conn = slots[i].connection;
+
+ if (conn == NULL)
+ continue;
+
+ disconnectDatabase(conn);
+ conn = NULL;
+ }
pfree(slots);
termPQExpBuffer(&sql);
@@ -953,262 +898,6 @@ run_vacuum_command(PGconn *conn, const char *sql, bool echo,
}
}
-/*
- * GetIdleSlot
- * Return a connection slot that is ready to execute a command.
- *
- * We return the first slot we find that is marked isFree, if one is;
- * otherwise, we loop on select() until one socket becomes available. When
- * this happens, we read the whole set and mark as free all sockets that become
- * available.
- *
- * If an error occurs, NULL is returned.
- */
-static ParallelSlot *
-GetIdleSlot(ParallelSlot slots[], int numslots,
- const char *progname)
-{
- int i;
- int firstFree = -1;
-
- /* Any connection already known free? */
- for (i = 0; i < numslots; i++)
- {
- if (slots[i].isFree)
- return slots + i;
- }
-
- /*
- * No free slot found, so wait until one of the connections has finished
- * its task and return the available slot.
- */
- while (firstFree < 0)
- {
- fd_set slotset;
- int maxFd = 0;
- bool aborting;
-
- /* We must reconstruct the fd_set for each call to select_loop */
- FD_ZERO(&slotset);
-
- for (i = 0; i < numslots; i++)
- {
- int sock = PQsocket(slots[i].connection);
-
- /*
- * We don't really expect any connections to lose their sockets
- * after startup, but just in case, cope by ignoring them.
- */
- if (sock < 0)
- continue;
-
- FD_SET(sock, &slotset);
- if (sock > maxFd)
- maxFd = sock;
- }
-
- SetCancelConn(slots->connection);
- i = select_loop(maxFd, &slotset, &aborting);
- ResetCancelConn();
-
- if (aborting)
- {
- /*
- * We set the cancel-receiving connection to the one in the zeroth
- * slot above, so fetch the error from there.
- */
- GetQueryResult(slots->connection, progname);
- return NULL;
- }
- Assert(i != 0);
-
- for (i = 0; i < numslots; i++)
- {
- int sock = PQsocket(slots[i].connection);
-
- if (sock >= 0 && FD_ISSET(sock, &slotset))
- {
- /* select() says input is available, so consume it */
- PQconsumeInput(slots[i].connection);
- }
-
- /* Collect result(s) as long as any are available */
- while (!PQisBusy(slots[i].connection))
- {
- PGresult *result = PQgetResult(slots[i].connection);
-
- if (result != NULL)
- {
- /* Check and discard the command result */
- if (!ProcessQueryResult(slots[i].connection, result,
- progname))
- return NULL;
- }
- else
- {
- /* This connection has become idle */
- slots[i].isFree = true;
- if (firstFree < 0)
- firstFree = i;
- break;
- }
- }
- }
- }
-
- return slots + firstFree;
-}
-
-/*
- * ProcessQueryResult
- *
- * Process (and delete) a query result. Returns true if there's no error,
- * false otherwise -- but errors about trying to vacuum a missing relation
- * are reported and subsequently ignored.
- */
-static bool
-ProcessQueryResult(PGconn *conn, PGresult *result, const char *progname)
-{
- /*
- * If it's an error, report it. Errors about a missing table are harmless
- * so we continue processing; but die for other errors.
- */
- if (PQresultStatus(result) != PGRES_COMMAND_OK)
- {
- char *sqlState = PQresultErrorField(result, PG_DIAG_SQLSTATE);
-
- pg_log_error("vacuuming of database \"%s\" failed: %s",
- PQdb(conn), PQerrorMessage(conn));
-
- if (sqlState && strcmp(sqlState, ERRCODE_UNDEFINED_TABLE) != 0)
- {
- PQclear(result);
- return false;
- }
- }
-
- PQclear(result);
- return true;
-}
-
-/*
- * GetQueryResult
- *
- * Pump the conn till it's dry of results; return false if any are errors.
- * Note that this will block if the conn is busy.
- */
-static bool
-GetQueryResult(PGconn *conn, const char *progname)
-{
- bool ok = true;
- PGresult *result;
-
- SetCancelConn(conn);
- while ((result = PQgetResult(conn)) != NULL)
- {
- if (!ProcessQueryResult(conn, result, progname))
- ok = false;
- }
- ResetCancelConn();
- return ok;
-}
-
-/*
- * DisconnectDatabase
- * Disconnect the connection associated with the given slot
- */
-static void
-DisconnectDatabase(ParallelSlot *slot)
-{
- char errbuf[256];
-
- if (!slot->connection)
- return;
-
- if (PQtransactionStatus(slot->connection) == PQTRANS_ACTIVE)
- {
- PGcancel *cancel;
-
- if ((cancel = PQgetCancel(slot->connection)))
- {
- (void) PQcancel(cancel, errbuf, sizeof(errbuf));
- PQfreeCancel(cancel);
- }
- }
-
- PQfinish(slot->connection);
- slot->connection = NULL;
-}
-
-/*
- * Loop on select() until a descriptor from the given set becomes readable.
- *
- * If we get a cancel request while we're waiting, we forego all further
- * processing and set the *aborting flag to true. The return value must be
- * ignored in this case. Otherwise, *aborting is set to false.
- */
-static int
-select_loop(int maxFd, fd_set *workerset, bool *aborting)
-{
- int i;
- fd_set saveSet = *workerset;
-
- if (CancelRequested)
- {
- *aborting = true;
- return -1;
- }
- else
- *aborting = false;
-
- for (;;)
- {
- /*
- * On Windows, we need to check once in a while for cancel requests;
- * on other platforms we rely on select() returning when interrupted.
- */
- struct timeval *tvp;
-#ifdef WIN32
- struct timeval tv = {0, 1000000};
-
- tvp = &tv;
-#else
- tvp = NULL;
-#endif
-
- *workerset = saveSet;
- i = select(maxFd + 1, workerset, NULL, NULL, tvp);
-
-#ifdef WIN32
- if (i == SOCKET_ERROR)
- {
- i = -1;
-
- if (WSAGetLastError() == WSAEINTR)
- errno = EINTR;
- }
-#endif
-
- if (i < 0 && errno == EINTR)
- continue; /* ignore this */
- if (i < 0 || CancelRequested)
- *aborting = true; /* but not this */
- if (i == 0)
- continue; /* timeout (Win32 only) */
- break;
- }
-
- return i;
-}
-
-static void
-init_slot(ParallelSlot *slot, PGconn *conn)
-{
- slot->connection = conn;
- /* Initially assume connection is idle */
- slot->isFree = true;
-}
-
static void
help(const char *progname)
{
--
2.20.1
On Tue, Jul 09, 2019 at 01:09:38PM +0200, Peter Eisentraut wrote:
You can already do that: Run a query through psql to get a list of
affected tables or indexes and feed those to reindexdb using -i or -t
options.
Sure, but that's limited if one can only afford a limited amount of
downtime for an upgrade window and you still need to handle properly
the index-level conflicts when doing the processing in parallel.
--
Michael
On 2019-Jul-09, Julien Rouhaud wrote:
I finished to do a better refactoring, and ended up with this API in
scripts_parallel:
Looking good! I'm not sure about the "Consume" word in ConsumeIdleSlot;
maybe "Reserve"? "Obtain"? "Get"?
Code commentary: I think the comment that sits atop the function should
describe what the function does without getting too much in how it does
it. For example in ConsumeIdleSlot you have "If there are multiples
slots, here we wait for one connection to become available if none
already is, returning NULL if an error occured. Otherwise, we simply
use the only slot we have, which we know to be free." which seems like
it should be in another comment *inside* the function; make the external
one something like "Reserve and return a connection that is currently
idle, waiting until one becomes idle if none is". Maybe you can put the
part I first quoted as a second paragraph in the comment at top of
function and keeping the second part I quoted as first paragraph; we
seem to use that style too.
Placement: I think it's good if related functions stay together, or
there is some other rationale for placement within the file. I have two
favorite approaches: one is to put all externally callable functions at
top of file, followed by all the static helpers in the lower half of the
file. The other is to put each externally accessible immediately
followed by its specific static helpers. If you choose one of those,
that means that SetupParallelSlots should either move upwards, or move
downwards. The current ordering seems a dartboard kind of thing where
the thrower is not Green Arrow.
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Hi Alvaro,
Thanks a lot for the review
On Wed, Jul 10, 2019 at 4:15 PM Alvaro Herrera <alvherre@2ndquadrant.com> wrote:
On 2019-Jul-09, Julien Rouhaud wrote:
I finished to do a better refactoring, and ended up with this API in
scripts_parallel:Looking good!
Thanks!
I'm not sure about the "Consume" word in ConsumeIdleSlot;
maybe "Reserve"? "Obtain"? "Get"?
Yes, Consume is maybe a little bit weird. I wanted to point out the
make it clear that this function is actually removing a slot from the
free list, especially since there's a (historical) get_idle_slot(). I
like Reserve, but Obtain and Get are probably too ambiguous.
Code commentary: I think the comment that sits atop the function should
describe what the function does without getting too much in how it does
it. For example in ConsumeIdleSlot you have "If there are multiples
slots, here we wait for one connection to become available if none
already is, returning NULL if an error occured. Otherwise, we simply
use the only slot we have, which we know to be free." which seems like
it should be in another comment *inside* the function; make the external
one something like "Reserve and return a connection that is currently
idle, waiting until one becomes idle if none is". Maybe you can put the
part I first quoted as a second paragraph in the comment at top of
function and keeping the second part I quoted as first paragraph; we
seem to use that style too.
Good point, I'll fix as you say.
Placement: I think it's good if related functions stay together, or
there is some other rationale for placement within the file. I have two
favorite approaches: one is to put all externally callable functions at
top of file, followed by all the static helpers in the lower half of the
file. The other is to put each externally accessible immediately
followed by its specific static helpers. If you choose one of those,
that means that SetupParallelSlots should either move upwards, or move
downwards. The current ordering seems a dartboard kind of thing where
the thrower is not Green Arrow.
:) I tried to put everything in alphabetic order as it was previously
being done, but I apparently failed again at sorting more than 3
characters.
I usually prefer to group exported functions together and static ones
together, as I find it more maintainable in the long term, so upwards
it'll be.
On Wed, Jul 10, 2019 at 09:44:14PM +0200, Julien Rouhaud wrote:
On Wed, Jul 10, 2019 at 4:15 PM Alvaro Herrera <alvherre@2ndquadrant.com> wrote:
Looking good!
Thanks!
Confirmed. The last set is much easier to go through.
I'm not sure about the "Consume" word in ConsumeIdleSlot;
maybe "Reserve"? "Obtain"? "Get"?Yes, Consume is maybe a little bit weird. I wanted to point out the
make it clear that this function is actually removing a slot from the
free list, especially since there's a (historical) get_idle_slot(). I
like Reserve, but Obtain and Get are probably too ambiguous.
The refactoring patch is getting in shape. Now reindex_one_database()
is the only place setting and manipulating the slots. I am wondering
if we should have a wrapper which disconnects all the slots (doing
conn = NULL after the disconnectDatabase() call does not matter).
Get* would be my choice, because we look at the set of parallel slots,
and get an idle one. It would be nice to have more consistency in the
names for the routines, say:
- ParallelSlotInit() instead of SetupParallelSlots (still my
suggestion is not perfect either as that sounds like one single slot,
but we have a set of these).
- ParallelSlotGetIdle() instead of ConsumeIdleSlot(). Still that's
more a wait-then-get behavior.
- ParallelSlotWaitCompletion() instead of WaitForSlotsCompletion()
- ParallelSlotDisconnect, as a wrapper for the calls to
DisconnectDatabase().
Placement: I think it's good if related functions stay together, or
there is some other rationale for placement within the file. I have two
favorite approaches: one is to put all externally callable functions at
top of file, followed by all the static helpers in the lower half of the
file. The other is to put each externally accessible immediately
followed by its specific static helpers. If you choose one of those,
that means that SetupParallelSlots should either move upwards, or move
downwards. The current ordering seems a dartboard kind of thing where
the thrower is not Green Arrow.I usually prefer to group exported functions together and static ones
together, as I find it more maintainable in the long term, so upwards
it'll be.
That's mainly a matter of taste. Depending on the code path in the
tree, sometimes the two approaches from above are used. We have some
other files where the static routines are listed first at the top,
followed by the exported ones at the bottom as it saves from some
declarations of the functions at the top of the file. Keeping the
footprint of the author is not that bad either, and that depends also
on the context. For this one, as the static functions are linked to
the exported ones in a close manner, I would keep each set grouped
though.
+ /*
+ * Database-wide parallel reindex requires special processing. If
+ * multiple jobs were asked, we have to reindex system catalogs first,
+ * as they can't be processed in parallel.
+ */
+ if (process_type == REINDEX_DATABASE)
In patch 0002, a parallel database REINDEX first processes the catalog
relations in a serializable fashion, and then all the other relations
in parallel, which is right Could we have schema-level reindexes also
process things in parallel with all the relations from all the schemas
listed? This would be profitable in particular for callers listing
multiple schemas with an unbalanced number of tables in each, and we'd
need to be careful of the same where pg_catalog is listed. Actually,
your patch breaks if we do a parallel run with pg_catalog and another
schema, no?
--
Michael
On Thu, Jul 11, 2019 at 6:04 AM Michael Paquier <michael@paquier.xyz> wrote:
On Wed, Jul 10, 2019 at 09:44:14PM +0200, Julien Rouhaud wrote:
On Wed, Jul 10, 2019 at 4:15 PM Alvaro Herrera <alvherre@2ndquadrant.com> wrote:
Looking good!
Thanks!
Confirmed. The last set is much easier to go through.
I'm not sure about the "Consume" word in ConsumeIdleSlot;
maybe "Reserve"? "Obtain"? "Get"?Yes, Consume is maybe a little bit weird. I wanted to point out the
make it clear that this function is actually removing a slot from the
free list, especially since there's a (historical) get_idle_slot(). I
like Reserve, but Obtain and Get are probably too ambiguous.The refactoring patch is getting in shape. Now reindex_one_database()
is the only place setting and manipulating the slots. I am wondering
if we should have a wrapper which disconnects all the slots (doing
conn = NULL after the disconnectDatabase() call does not matter).
You already mentioned that in a previous mail. I was afraid it'd be
overkill, but it'll make caller code easier, so let's do it.
Get* would be my choice, because we look at the set of parallel slots,
and get an idle one.
That's what the former GetIdleSlot (that I renamed to get_idle_slot as
it's not static) is doing. ConsumeIdleSlot() actually get an idle
slot and mark it as being used. That's probably some leakage of
internal implementation, but having a GetIdleParallelSlot (or
ParallelSlotGetIdle) *and* a get_idle_slot sounds like a bad idea, and
I don't have a better idea on how to rename get_idle_slot. If you
really prefer Get* and you're fine with a static get_idle_slot, that's
fine by me.
It would be nice to have more consistency in the
names for the routines, say:
- ParallelSlotInit() instead of SetupParallelSlots (still my
suggestion is not perfect either as that sounds like one single slot,
but we have a set of these).
- ParallelSlotGetIdle() instead of ConsumeIdleSlot(). Still that's
more a wait-then-get behavior.
- ParallelSlotWaitCompletion() instead of WaitForSlotsCompletion()
- ParallelSlotDisconnect, as a wrapper for the calls to
DisconnectDatabase().
I don't have an opinion on whether to use parallel slot as prefix or
postfix, so I'm fine with postfixing.
+ /* + * Database-wide parallel reindex requires special processing. If + * multiple jobs were asked, we have to reindex system catalogs first, + * as they can't be processed in parallel. + */ + if (process_type == REINDEX_DATABASE)In patch 0002, a parallel database REINDEX first processes the catalog
relations in a serializable fashion, and then all the other relations
in parallel, which is right Could we have schema-level reindexes also
process things in parallel with all the relations from all the schemas
listed? This would be profitable in particular for callers listing
multiple schemas with an unbalanced number of tables in each
It would also be beneficial for a parallel reindex of a single schema.
and we'd
need to be careful of the same where pg_catalog is listed. Actually,
your patch breaks if we do a parallel run with pg_catalog and another
schema, no?
It can definitely cause problems if you ask for pg_catalog and other
schema, same as if you ask a parallel reindex of some catalog tables
(possibly with other tables). There's a --system switch for that
need, so I don't know if documenting the limitation to avoid some
extra code to deal with it is a good enough solution?
On Thu, Jul 11, 2019 at 11:48:20AM +0200, Julien Rouhaud wrote:
On Thu, Jul 11, 2019 at 6:04 AM Michael Paquier <michael@paquier.xyz> wrote:
Get* would be my choice, because we look at the set of parallel slots,
and get an idle one.That's what the former GetIdleSlot (that I renamed to get_idle_slot as
it's not static) is doing. ConsumeIdleSlot() actually get an idle
slot and mark it as being used. That's probably some leakage of
internal implementation, but having a GetIdleParallelSlot (or
ParallelSlotGetIdle) *and* a get_idle_slot sounds like a bad idea, and
I don't have a better idea on how to rename get_idle_slot. If you
really prefer Get* and you're fine with a static get_idle_slot, that's
fine by me.
Do we actually need get_idle_slot? ConsumeIdleSlot is its only
caller.
and we'd
need to be careful of the same where pg_catalog is listed. Actually,
your patch breaks if we do a parallel run with pg_catalog and another
schema, no?It can definitely cause problems if you ask for pg_catalog and other
schema, same as if you ask a parallel reindex of some catalog tables
(possibly with other tables). There's a --system switch for that
need, so I don't know if documenting the limitation to avoid some
extra code to deal with it is a good enough solution?
vacuumdb --full still has limitations in this area and we had some
reports on the matter about this behavior being annoying. Its
documentation also mentions that mixing catalog relations with --full
can cause deadlocks.
Documenting it may be fine at the end, but my take is that it would be
nice to make sure that we don't have deadlocks if we can avoid them
easily. It is also a matter of balance. If for example the patch
gets 3 times bigger in size because of that we may have an argument
for not doing it and keep the code simple. What do people think about
that? I would be nice to get more opinions here.
And while scanning the code...
+ * getQuerySucess
Typo here.
- * Pump the conn till it's dry of results; return false if any are errors.
This comment could be improved on the way, like "Go through all the
connections and make sure to consume any remaining results. If any
error is found, false is returned after processing all the parallel
slots."
--
Michael
On Thu, Jul 11, 2019 at 3:34 PM Michael Paquier <michael@paquier.xyz> wrote:
On Thu, Jul 11, 2019 at 11:48:20AM +0200, Julien Rouhaud wrote:
On Thu, Jul 11, 2019 at 6:04 AM Michael Paquier <michael@paquier.xyz> wrote:
Get* would be my choice, because we look at the set of parallel slots,
and get an idle one.That's what the former GetIdleSlot (that I renamed to get_idle_slot as
it's not static) is doing. ConsumeIdleSlot() actually get an idle
slot and mark it as being used. That's probably some leakage of
internal implementation, but having a GetIdleParallelSlot (or
ParallelSlotGetIdle) *and* a get_idle_slot sounds like a bad idea, and
I don't have a better idea on how to rename get_idle_slot. If you
really prefer Get* and you're fine with a static get_idle_slot, that's
fine by me.Do we actually need get_idle_slot? ConsumeIdleSlot is its only
caller.
I think t hat it makes the code quite cleaner to have the selection
outside ConsumeIdleSlot.
And while scanning the code...
+ * getQuerySucess
Typo here.
Argh, I thought I caught all of them, thanks!
- * Pump the conn till it's dry of results; return false if any are errors.
This comment could be improved on the way, like "Go through all the
connections and make sure to consume any remaining results. If any
error is found, false is returned after processing all the parallel
slots."
You're talking about getQuerySuccess right? That was actually the
original comment of a function I renamed. +1 to improve it, but this
function is in common.c and doesn't deal with parallel slot at all, so
I'll just drop the slang parts.
On Thu, Jul 11, 2019 at 06:22:25PM +0200, Julien Rouhaud wrote:
I think t hat it makes the code quite cleaner to have the selection
outside ConsumeIdleSlot.
Actually, you have an issue with ConsumeIdleSlot() if there is only
one parallel slot, no? In this case the current patch returns
immediately the slot available without waiting. I think that we
should wait until the slot becomes free in that case as well, and
switch isFree to false. If you want to keep things splitted, that's
fine by me, I would still use "Get" within the name for the routine,
and rename the other to get_idle_slot_internal() or
get_idle_slot_guts() to point out that it has an internal role.
You're talking about getQuerySuccess right? That was actually the
original comment of a function I renamed. +1 to improve it, but this
function is in common.c and doesn't deal with parallel slot at all, so
I'll just drop the slang parts.
If we can design a clean interface with better comments, we can use
this occasion to browse the whole thing and make it better.
--
Michael
On Fri, Jul 12, 2019 at 3:20 AM Michael Paquier <michael@paquier.xyz> wrote:
On Thu, Jul 11, 2019 at 06:22:25PM +0200, Julien Rouhaud wrote:
I think t hat it makes the code quite cleaner to have the selection
outside ConsumeIdleSlot.Actually, you have an issue with ConsumeIdleSlot() if there is only
one parallel slot, no? In this case the current patch returns
immediately the slot available without waiting. I think that we
should wait until the slot becomes free in that case as well, and
switch isFree to false.
It shouldn't be a problem, I reused the same infrastructure as for
vacuumdb. so run_reindex_command has a new "async" parameter, so when
there's no parallelism it's using executeMaintenanceCommand (instead
of PQsendQuery) which will block until query completion. That's why
there's no isFree usage at all in this case.
If you want to keep things splitted, that's
fine by me, I would still use "Get" within the name for the routine,
and rename the other to get_idle_slot_internal() or
get_idle_slot_guts() to point out that it has an internal role.
Ok, I'll change to get_idle_slot_internal then.
On Fri, Jul 12, 2019 at 07:49:13AM +0200, Julien Rouhaud wrote:
It shouldn't be a problem, I reused the same infrastructure as for
vacuumdb. so run_reindex_command has a new "async" parameter, so when
there's no parallelism it's using executeMaintenanceCommand (instead
of PQsendQuery) which will block until query completion. That's why
there's no isFree usage at all in this case.
My point is more about consistency and simplification with the case
where n > 1 and that we could actually move the async/sync code paths
into the same banner as the async mode waits as well until a slot is
free, or in short when the query completes.
--
Michael
On Fri, Jul 12, 2019 at 7:57 AM Michael Paquier <michael@paquier.xyz> wrote:
On Fri, Jul 12, 2019 at 07:49:13AM +0200, Julien Rouhaud wrote:
It shouldn't be a problem, I reused the same infrastructure as for
vacuumdb. so run_reindex_command has a new "async" parameter, so when
there's no parallelism it's using executeMaintenanceCommand (instead
of PQsendQuery) which will block until query completion. That's why
there's no isFree usage at all in this case.My point is more about consistency and simplification with the case
where n > 1 and that we could actually move the async/sync code paths
into the same banner as the async mode waits as well until a slot is
free, or in short when the query completes.
I attach v4 with all previous comment addressed.
I also changed to handle parallel and non-parallel case the same way.
I kept the possibility for synchronous behavior in reindexdb, as
there's an early need to run some queries in case of parallel
database-wide reindex. It avoids to open all the connections in case
anything fails during this preliminary work, and it also avoids
another call for the async wait function. If we add parallelism to
clusterdb (I'll probably work on that next time I have spare time),
reindexdb would be the only caller left of
executeMaintenanceCommand(), so that's something we may want to
change.
I didn't change the behavior wrt. possible deadlock if user specify
catalog objects using --index or --table and ask for multiple
connection, as I'm afraid that it'll add too much code for a little
benefit. Please shout if you think otherwise.
Attachments:
0001-Export-vacuumdb-s-parallel-infrastructure-v4.patchapplication/octet-stream; name=0001-Export-vacuumdb-s-parallel-infrastructure-v4.patchDownload
From de079a6e777c0c8381f80478f121b09c173d7909 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 28 Jun 2019 13:11:29 +0200
Subject: [PATCH 1/2] Export vacuumdb's parallel infrastructure
---
src/bin/scripts/Makefile | 4 +-
src/bin/scripts/common.c | 79 ++++++
src/bin/scripts/common.h | 7 +
src/bin/scripts/scripts_parallel.c | 264 ++++++++++++++++++++
src/bin/scripts/scripts_parallel.h | 34 +++
src/bin/scripts/vacuumdb.c | 374 ++---------------------------
6 files changed, 407 insertions(+), 355 deletions(-)
create mode 100644 src/bin/scripts/scripts_parallel.c
create mode 100644 src/bin/scripts/scripts_parallel.h
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 9f352b5e2b..3cd793b134 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -28,7 +28,7 @@ createuser: createuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport
dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-vacuumdb: vacuumdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
@@ -50,7 +50,7 @@ uninstall:
clean distclean maintainer-clean:
rm -f $(addsuffix $(X), $(PROGRAMS)) $(addsuffix .o, $(PROGRAMS))
- rm -f common.o $(WIN32RES)
+ rm -f common.o scripts_parallel.o $(WIN32RES)
rm -rf tmp_check
check:
diff --git a/src/bin/scripts/common.c b/src/bin/scripts/common.c
index 296029d809..8d7cb252b5 100644
--- a/src/bin/scripts/common.c
+++ b/src/bin/scripts/common.c
@@ -22,6 +22,8 @@
#include "fe_utils/connect.h"
#include "fe_utils/string_utils.h"
+#define ERRCODE_UNDEFINED_TABLE "42P01"
+
static PGcancel *volatile cancelConn = NULL;
bool CancelRequested = false;
@@ -178,6 +180,28 @@ connectMaintenanceDatabase(const char *maintenance_db,
return conn;
}
+/*
+ * Disconnect the given connection, canceling any statement if one is active.
+ */
+void
+disconnectDatabase(PGconn *conn)
+{
+ char errbuf[256];
+
+ if (PQtransactionStatus(conn) == PQTRANS_ACTIVE)
+ {
+ PGcancel *cancel;
+
+ if ((cancel = PQgetCancel(conn)))
+ {
+ (void) PQcancel(cancel, errbuf, sizeof(errbuf));
+ PQfreeCancel(cancel);
+ }
+ }
+
+ PQfinish(conn);
+}
+
/*
* Run a query, return the results, exit program on failure.
*/
@@ -255,6 +279,61 @@ executeMaintenanceCommand(PGconn *conn, const char *query, bool echo)
return r;
}
+/*
+ * getQuerySucess
+ *
+ * Process the given connection's result until it's depleted. Return false if
+ * error was encountered.
+ * Note that this will block if the conn is busy.
+ */
+bool
+getQuerySuccess(PGconn *conn, const char *progname)
+{
+ bool ok = true;
+ PGresult *result;
+
+ SetCancelConn(conn);
+ while ((result = PQgetResult(conn)) != NULL)
+ {
+ if (!processQueryResult(conn, result, progname))
+ ok = false;
+ }
+ ResetCancelConn();
+ return ok;
+}
+
+/*
+ * processQueryResult
+ *
+ * Process (and delete) a query result. Returns true if there's no error,
+ * false otherwise -- but errors about trying to vacuum a missing relation
+ * are reported and subsequently ignored.
+ */
+bool
+processQueryResult(PGconn *conn, PGresult *result, const char *progname)
+{
+ /*
+ * If it's an error, report it. Errors about a missing table are harmless
+ * so we continue processing; but die for other errors.
+ */
+ if (PQresultStatus(result) != PGRES_COMMAND_OK)
+ {
+ char *sqlState = PQresultErrorField(result, PG_DIAG_SQLSTATE);
+
+ pg_log_error("processing of database \"%s\" failed: %s",
+ PQdb(conn), PQerrorMessage(conn));
+
+ if (sqlState && strcmp(sqlState, ERRCODE_UNDEFINED_TABLE) != 0)
+ {
+ PQclear(result);
+ return false;
+ }
+ }
+
+ PQclear(result);
+ return true;
+}
+
/*
* Split TABLE[(COLUMNS)] into TABLE and [(COLUMNS)] portions. When you
diff --git a/src/bin/scripts/common.h b/src/bin/scripts/common.h
index 35d1a3e0d5..9f10e1da96 100644
--- a/src/bin/scripts/common.h
+++ b/src/bin/scripts/common.h
@@ -39,6 +39,8 @@ extern PGconn *connectMaintenanceDatabase(const char *maintenance_db,
const char *pguser, enum trivalue prompt_password,
const char *progname, bool echo);
+extern void disconnectDatabase(PGconn *conn);
+
extern PGresult *executeQuery(PGconn *conn, const char *query,
const char *progname, bool echo);
@@ -48,6 +50,11 @@ extern void executeCommand(PGconn *conn, const char *query,
extern bool executeMaintenanceCommand(PGconn *conn, const char *query,
bool echo);
+extern bool getQuerySuccess(PGconn *conn, const char *progname);
+
+extern bool processQueryResult(PGconn *conn, PGresult *result,
+ const char *progname);
+
extern void splitTableColumnsSpec(const char *spec, int encoding,
char **table, const char **columns);
diff --git a/src/bin/scripts/scripts_parallel.c b/src/bin/scripts/scripts_parallel.c
new file mode 100644
index 0000000000..3999f6f6e4
--- /dev/null
+++ b/src/bin/scripts/scripts_parallel.c
@@ -0,0 +1,264 @@
+/*-------------------------------------------------------------------------
+ *
+ * scripts_parallel.c
+ * Parallel support for bin/scripts/
+ *
+ *
+ * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/bin/scripts/scripts_parallel.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#ifdef HAVE_SYS_SELECT_H
+#include <sys/select.h>
+#endif
+
+#include "common.h"
+#include "common/logging.h"
+#include "scripts_parallel.h"
+
+static void init_slot(ParallelSlot *slot, PGconn *conn);
+static int select_loop(int maxFd, fd_set *workerset, bool *aborting);
+
+static void
+init_slot(ParallelSlot *slot, PGconn *conn)
+{
+ slot->connection = conn;
+ /* Initially assume connection is idle */
+ slot->isFree = true;
+}
+
+/*
+ * Loop on select() until a descriptor from the given set becomes readable.
+ *
+ * If we get a cancel request while we're waiting, we forego all further
+ * processing and set the *aborting flag to true. The return value must be
+ * ignored in this case. Otherwise, *aborting is set to false.
+ */
+static int
+select_loop(int maxFd, fd_set *workerset, bool *aborting)
+{
+ int i;
+ fd_set saveSet = *workerset;
+
+ if (CancelRequested)
+ {
+ *aborting = true;
+ return -1;
+ }
+ else
+ *aborting = false;
+
+ for (;;)
+ {
+ /*
+ * On Windows, we need to check once in a while for cancel requests;
+ * on other platforms we rely on select() returning when interrupted.
+ */
+ struct timeval *tvp;
+#ifdef WIN32
+ struct timeval tv = {0, 1000000};
+
+ tvp = &tv;
+#else
+ tvp = NULL;
+#endif
+
+ *workerset = saveSet;
+ i = select(maxFd + 1, workerset, NULL, NULL, tvp);
+
+#ifdef WIN32
+ if (i == SOCKET_ERROR)
+ {
+ i = -1;
+
+ if (WSAGetLastError() == WSAEINTR)
+ errno = EINTR;
+ }
+#endif
+
+ if (i < 0 && errno == EINTR)
+ continue; /* ignore this */
+ if (i < 0 || CancelRequested)
+ *aborting = true; /* but not this */
+ if (i == 0)
+ continue; /* timeout (Win32 only) */
+ break;
+ }
+
+ return i;
+}
+
+/*
+ * get_idle_slot
+ * Return a connection slot that is ready to execute a command.
+ *
+ * We return the first slot we find that is marked isFree, if one is;
+ * otherwise, we loop on select() until one socket becomes available. When
+ * this happens, we read the whole set and mark as free all sockets that become
+ * available.
+ *
+ * If an error occurs, NULL is returned.
+ */
+ParallelSlot *
+ParallelSlotsGetIdle(ParallelSlot *slots, int numslots,
+ const char *progname)
+{
+ int i;
+ int firstFree = -1;
+
+ /* Any connection already known free? */
+ for (i = 0; i < numslots; i++)
+ {
+ if (slots[i].isFree)
+ {
+ slots[i].isFree = false;
+ return slots + i;
+ }
+ }
+
+ /*
+ * No free slot found, so wait until one of the connections has finished
+ * its task and return the available slot.
+ */
+ while (firstFree < 0)
+ {
+ fd_set slotset;
+ int maxFd = 0;
+ bool aborting;
+
+ /* We must reconstruct the fd_set for each call to select_loop */
+ FD_ZERO(&slotset);
+
+ for (i = 0; i < numslots; i++)
+ {
+ int sock = PQsocket(slots[i].connection);
+
+ /*
+ * We don't really expect any connections to lose their sockets
+ * after startup, but just in case, cope by ignoring them.
+ */
+ if (sock < 0)
+ continue;
+
+ FD_SET(sock, &slotset);
+ if (sock > maxFd)
+ maxFd = sock;
+ }
+
+ SetCancelConn(slots->connection);
+ i = select_loop(maxFd, &slotset, &aborting);
+ ResetCancelConn();
+
+ if (aborting)
+ {
+ /*
+ * We set the cancel-receiving connection to the one in the zeroth
+ * slot above, so fetch the error from there.
+ */
+ getQuerySuccess(slots->connection, progname);
+ return NULL;
+ }
+ Assert(i != 0);
+
+ for (i = 0; i < numslots; i++)
+ {
+ int sock = PQsocket(slots[i].connection);
+
+ if (sock >= 0 && FD_ISSET(sock, &slotset))
+ {
+ /* select() says input is available, so consume it */
+ PQconsumeInput(slots[i].connection);
+ }
+
+ /* Collect result(s) as long as any are available */
+ while (!PQisBusy(slots[i].connection))
+ {
+ PGresult *result = PQgetResult(slots[i].connection);
+
+ if (result != NULL)
+ {
+ /* Check and discard the command result */
+ if (!processQueryResult(slots[i].connection, result,
+ progname))
+ return NULL;
+ }
+ else
+ {
+ /* This connection has become idle */
+ slots[i].isFree = true;
+ if (firstFree < 0)
+ firstFree = i;
+ break;
+ }
+ }
+ }
+ }
+
+ slots[firstFree].isFree = false;
+ return slots + firstFree;
+}
+
+ParallelSlot *
+ParallelSlotsSetup(const char *dbname, const char *host, const char *port,
+ const char *username, bool prompt_password, const char *progname,
+ bool echo, PGconn *conn, int numslots)
+{
+ ParallelSlot *slots;
+ int i;
+
+ slots = (ParallelSlot *) pg_malloc(sizeof(ParallelSlot) * numslots);
+ init_slot(slots, conn);
+ if (numslots > 1)
+ {
+ for (i = 1; i < numslots; i++)
+ {
+ conn = connectDatabase(dbname, host, port, username, prompt_password,
+ progname, echo, false, true);
+ init_slot(slots + i, conn);
+ }
+ }
+
+ return slots;
+}
+
+/*
+ * Iterate thhrough all connections in a given array of ParallelSlot and
+ * terminate all connections.
+ */
+void
+ParallelSlotsTerminate(ParallelSlot *slots, int numslots)
+{
+ int i;
+
+ for (i = 0; i < numslots; i++)
+ {
+ PGconn *conn = slots[i].connection;
+
+ if (conn == NULL)
+ continue;
+
+ disconnectDatabase(conn);
+ conn = NULL;
+ }
+}
+
+/* Wait for all connections to finish, returning true if no error occured.. */
+bool
+ParallelSlotsWaitCompletion(ParallelSlot *slots, int numslots, const char *progname)
+{
+ int i;
+
+ for (i = 0; i < numslots; i++)
+ {
+ if (!getQuerySuccess((slots + i)->connection, progname))
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/bin/scripts/scripts_parallel.h b/src/bin/scripts/scripts_parallel.h
new file mode 100644
index 0000000000..b98dfc8936
--- /dev/null
+++ b/src/bin/scripts/scripts_parallel.h
@@ -0,0 +1,34 @@
+/*
+ * scripts_parallel.h
+ * Parallel support for bin/scripts/
+ *
+ * Copyright (c) 2003-2019, PostgreSQL Global Development Group
+ *
+ * src/bin/scripts/scripts_parallel.h
+ */
+#ifndef SCRIPTS_PARALLEL_H
+#define SCRIPTS_PARALLEL_H
+
+/* Parallel processing stuff */
+typedef struct ParallelSlot
+{
+ PGconn *connection; /* One connection */
+ bool isFree; /* Is it known to be idle? */
+} ParallelSlot;
+
+extern ParallelSlot *ParallelSlotsGetIdle(ParallelSlot *slots, int numslots,
+ const char *progname);
+
+extern ParallelSlot *ParallelSlotsSetup(const char *dbname, const char *host,
+ const char *port,
+ const char *username, bool prompt_password,
+ const char *progname, bool echo,
+ PGconn *conn, int numslots);
+
+extern void ParallelSlotsTerminate(ParallelSlot *slots, int numslots);
+
+extern bool ParallelSlotsWaitCompletion(ParallelSlot *slots, int numslots,
+ const char *progname);
+
+
+#endif /* SCRIPTS_PARALLEL_H */
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 3bcd14b4dc..3a5150d66e 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -12,10 +12,6 @@
#include "postgres_fe.h"
-#ifdef HAVE_SYS_SELECT_H
-#include <sys/select.h>
-#endif
-
#include "catalog/pg_class_d.h"
#include "common.h"
@@ -23,17 +19,9 @@
#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
-#define ERRCODE_UNDEFINED_TABLE "42P01"
-
-/* Parallel vacuuming stuff */
-typedef struct ParallelSlot
-{
- PGconn *connection; /* One connection */
- bool isFree; /* Is it known to be idle? */
-} ParallelSlot;
-
/* vacuum options controlled by user flags */
typedef struct vacuumingOptions
{
@@ -69,21 +57,7 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
vacuumingOptions *vacopts, const char *table);
static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
- const char *table, const char *progname, bool async);
-
-static ParallelSlot *GetIdleSlot(ParallelSlot slots[], int numslots,
- const char *progname);
-
-static bool ProcessQueryResult(PGconn *conn, PGresult *result,
- const char *progname);
-
-static bool GetQueryResult(PGconn *conn, const char *progname);
-
-static void DisconnectDatabase(ParallelSlot *slot);
-
-static int select_loop(int maxFd, fd_set *workerset, bool *aborting);
-
-static void init_slot(ParallelSlot *slot, PGconn *conn);
+ const char *table, const char *progname);
static void help(const char *progname);
@@ -625,17 +599,9 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
*/
if (concurrentCons <= 0)
concurrentCons = 1;
- slots = (ParallelSlot *) pg_malloc(sizeof(ParallelSlot) * concurrentCons);
- init_slot(slots, conn);
- if (parallel)
- {
- for (i = 1; i < concurrentCons; i++)
- {
- conn = connectDatabase(dbname, host, port, username, prompt_password,
- progname, echo, false, true);
- init_slot(slots + i, conn);
- }
- }
+
+ slots = ParallelSlotsSetup(dbname, host, port, username, prompt_password,
+ progname, echo, conn, concurrentCons);
/*
* Prepare all the connections to run the appropriate analyze stage, if
@@ -666,29 +632,12 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
goto finish;
}
- /*
- * Get the connection slot to use. If in parallel mode, here we wait
- * for one connection to become available if none already is. In
- * non-parallel mode we simply use the only slot we have, which we
- * know to be free.
- */
- if (parallel)
+ free_slot = ParallelSlotsGetIdle(slots, concurrentCons, progname);
+ if (!free_slot)
{
- /*
- * Get a free slot, waiting until one becomes free if none
- * currently is.
- */
- free_slot = GetIdleSlot(slots, concurrentCons, progname);
- if (!free_slot)
- {
- failed = true;
- goto finish;
- }
-
- free_slot->isFree = false;
+ failed = true;
+ goto finish;
}
- else
- free_slot = slots;
prepare_vacuum_command(&sql, PQserverVersion(free_slot->connection),
vacopts, tabname);
@@ -696,32 +645,19 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
/*
* Execute the vacuum. If not in parallel mode, this terminates the
* program in case of an error. (The parallel case handles query
- * errors in ProcessQueryResult through GetIdleSlot.)
+ * errors in ProcessQueryResult through ParallelSlotsGetIdle.)
*/
run_vacuum_command(free_slot->connection, sql.data,
- echo, tabname, progname, parallel);
+ echo, tabname, progname);
cell = cell->next;
} while (cell != NULL);
- if (parallel)
- {
- int j;
-
- /* wait for all connections to finish */
- for (j = 0; j < concurrentCons; j++)
- {
- if (!GetQueryResult((slots + j)->connection, progname))
- {
- failed = true;
- goto finish;
- }
- }
- }
+ if (!ParallelSlotsWaitCompletion(slots, concurrentCons, progname))
+ failed = true;
finish:
- for (i = 0; i < concurrentCons; i++)
- DisconnectDatabase(slots + i);
+ ParallelSlotsTerminate(slots, concurrentCons);
pfree(slots);
termPQExpBuffer(&sql);
@@ -914,27 +850,21 @@ prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
}
/*
- * Send a vacuum/analyze command to the server. In async mode, return after
- * sending the command; else, wait for it to finish.
+ * Send a vacuum/analyze command to the server, returning after sending the
+ * command.
*
- * Any errors during command execution are reported to stderr. If async is
- * false, this function exits the program after reporting the error.
+ * Any errors during command execution are reported to stderr.
*/
static void
run_vacuum_command(PGconn *conn, const char *sql, bool echo,
- const char *table, const char *progname, bool async)
+ const char *table, const char *progname)
{
bool status;
- if (async)
- {
- if (echo)
- printf("%s\n", sql);
+ if (echo)
+ printf("%s\n", sql);
- status = PQsendQuery(conn, sql) == 1;
- }
- else
- status = executeMaintenanceCommand(conn, sql, echo);
+ status = PQsendQuery(conn, sql) == 1;
if (!status)
{
@@ -944,271 +874,9 @@ run_vacuum_command(PGconn *conn, const char *sql, bool echo,
else
pg_log_error("vacuuming of database \"%s\" failed: %s",
PQdb(conn), PQerrorMessage(conn));
-
- if (!async)
- {
- PQfinish(conn);
- exit(1);
- }
}
}
-/*
- * GetIdleSlot
- * Return a connection slot that is ready to execute a command.
- *
- * We return the first slot we find that is marked isFree, if one is;
- * otherwise, we loop on select() until one socket becomes available. When
- * this happens, we read the whole set and mark as free all sockets that become
- * available.
- *
- * If an error occurs, NULL is returned.
- */
-static ParallelSlot *
-GetIdleSlot(ParallelSlot slots[], int numslots,
- const char *progname)
-{
- int i;
- int firstFree = -1;
-
- /* Any connection already known free? */
- for (i = 0; i < numslots; i++)
- {
- if (slots[i].isFree)
- return slots + i;
- }
-
- /*
- * No free slot found, so wait until one of the connections has finished
- * its task and return the available slot.
- */
- while (firstFree < 0)
- {
- fd_set slotset;
- int maxFd = 0;
- bool aborting;
-
- /* We must reconstruct the fd_set for each call to select_loop */
- FD_ZERO(&slotset);
-
- for (i = 0; i < numslots; i++)
- {
- int sock = PQsocket(slots[i].connection);
-
- /*
- * We don't really expect any connections to lose their sockets
- * after startup, but just in case, cope by ignoring them.
- */
- if (sock < 0)
- continue;
-
- FD_SET(sock, &slotset);
- if (sock > maxFd)
- maxFd = sock;
- }
-
- SetCancelConn(slots->connection);
- i = select_loop(maxFd, &slotset, &aborting);
- ResetCancelConn();
-
- if (aborting)
- {
- /*
- * We set the cancel-receiving connection to the one in the zeroth
- * slot above, so fetch the error from there.
- */
- GetQueryResult(slots->connection, progname);
- return NULL;
- }
- Assert(i != 0);
-
- for (i = 0; i < numslots; i++)
- {
- int sock = PQsocket(slots[i].connection);
-
- if (sock >= 0 && FD_ISSET(sock, &slotset))
- {
- /* select() says input is available, so consume it */
- PQconsumeInput(slots[i].connection);
- }
-
- /* Collect result(s) as long as any are available */
- while (!PQisBusy(slots[i].connection))
- {
- PGresult *result = PQgetResult(slots[i].connection);
-
- if (result != NULL)
- {
- /* Check and discard the command result */
- if (!ProcessQueryResult(slots[i].connection, result,
- progname))
- return NULL;
- }
- else
- {
- /* This connection has become idle */
- slots[i].isFree = true;
- if (firstFree < 0)
- firstFree = i;
- break;
- }
- }
- }
- }
-
- return slots + firstFree;
-}
-
-/*
- * ProcessQueryResult
- *
- * Process (and delete) a query result. Returns true if there's no error,
- * false otherwise -- but errors about trying to vacuum a missing relation
- * are reported and subsequently ignored.
- */
-static bool
-ProcessQueryResult(PGconn *conn, PGresult *result, const char *progname)
-{
- /*
- * If it's an error, report it. Errors about a missing table are harmless
- * so we continue processing; but die for other errors.
- */
- if (PQresultStatus(result) != PGRES_COMMAND_OK)
- {
- char *sqlState = PQresultErrorField(result, PG_DIAG_SQLSTATE);
-
- pg_log_error("vacuuming of database \"%s\" failed: %s",
- PQdb(conn), PQerrorMessage(conn));
-
- if (sqlState && strcmp(sqlState, ERRCODE_UNDEFINED_TABLE) != 0)
- {
- PQclear(result);
- return false;
- }
- }
-
- PQclear(result);
- return true;
-}
-
-/*
- * GetQueryResult
- *
- * Pump the conn till it's dry of results; return false if any are errors.
- * Note that this will block if the conn is busy.
- */
-static bool
-GetQueryResult(PGconn *conn, const char *progname)
-{
- bool ok = true;
- PGresult *result;
-
- SetCancelConn(conn);
- while ((result = PQgetResult(conn)) != NULL)
- {
- if (!ProcessQueryResult(conn, result, progname))
- ok = false;
- }
- ResetCancelConn();
- return ok;
-}
-
-/*
- * DisconnectDatabase
- * Disconnect the connection associated with the given slot
- */
-static void
-DisconnectDatabase(ParallelSlot *slot)
-{
- char errbuf[256];
-
- if (!slot->connection)
- return;
-
- if (PQtransactionStatus(slot->connection) == PQTRANS_ACTIVE)
- {
- PGcancel *cancel;
-
- if ((cancel = PQgetCancel(slot->connection)))
- {
- (void) PQcancel(cancel, errbuf, sizeof(errbuf));
- PQfreeCancel(cancel);
- }
- }
-
- PQfinish(slot->connection);
- slot->connection = NULL;
-}
-
-/*
- * Loop on select() until a descriptor from the given set becomes readable.
- *
- * If we get a cancel request while we're waiting, we forego all further
- * processing and set the *aborting flag to true. The return value must be
- * ignored in this case. Otherwise, *aborting is set to false.
- */
-static int
-select_loop(int maxFd, fd_set *workerset, bool *aborting)
-{
- int i;
- fd_set saveSet = *workerset;
-
- if (CancelRequested)
- {
- *aborting = true;
- return -1;
- }
- else
- *aborting = false;
-
- for (;;)
- {
- /*
- * On Windows, we need to check once in a while for cancel requests;
- * on other platforms we rely on select() returning when interrupted.
- */
- struct timeval *tvp;
-#ifdef WIN32
- struct timeval tv = {0, 1000000};
-
- tvp = &tv;
-#else
- tvp = NULL;
-#endif
-
- *workerset = saveSet;
- i = select(maxFd + 1, workerset, NULL, NULL, tvp);
-
-#ifdef WIN32
- if (i == SOCKET_ERROR)
- {
- i = -1;
-
- if (WSAGetLastError() == WSAEINTR)
- errno = EINTR;
- }
-#endif
-
- if (i < 0 && errno == EINTR)
- continue; /* ignore this */
- if (i < 0 || CancelRequested)
- *aborting = true; /* but not this */
- if (i == 0)
- continue; /* timeout (Win32 only) */
- break;
- }
-
- return i;
-}
-
-static void
-init_slot(ParallelSlot *slot, PGconn *conn)
-{
- slot->connection = conn;
- /* Initially assume connection is idle */
- slot->isFree = true;
-}
-
static void
help(const char *progname)
{
--
2.20.1
0002-Add-parallel-processing-to-reindexdb-v4.patchapplication/octet-stream; name=0002-Add-parallel-processing-to-reindexdb-v4.patchDownload
From 118314803448851b6c613adcf7a125edc63b0ad7 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 28 Jun 2019 13:21:58 +0200
Subject: [PATCH 2/2] Add parallel processing to reindexdb
---
doc/src/sgml/ref/reindexdb.sgml | 23 +++
src/bin/scripts/Makefile | 2 +-
src/bin/scripts/reindexdb.c | 301 ++++++++++++++++++++++++-----
src/bin/scripts/t/090_reindexdb.pl | 14 +-
4 files changed, 290 insertions(+), 50 deletions(-)
diff --git a/doc/src/sgml/ref/reindexdb.sgml b/doc/src/sgml/ref/reindexdb.sgml
index 25b5a72770..a7031030b9 100644
--- a/doc/src/sgml/ref/reindexdb.sgml
+++ b/doc/src/sgml/ref/reindexdb.sgml
@@ -166,6 +166,29 @@ PostgreSQL documentation
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>-j <replaceable class="parameter">njobs</replaceable></option></term>
+ <term><option>--jobs=<replaceable class="parameter">njobs</replaceable></option></term>
+ <listitem>
+ <para>
+ Execute the reindex commands in parallel by running
+ <replaceable class="parameter">njobs</replaceable>
+ commands simultaneously. This option reduces the time of the
+ processing but it also increases the load on the database server.
+ </para>
+ <para>
+ <application>reindexdb</application> will open
+ <replaceable class="parameter">njobs</replaceable> connections to the
+ database, so make sure your <xref linkend="guc-max-connections"/>
+ setting is high enough to accommodate all connections.
+ </para>
+ <para>
+ Note that this mode is not compatible the <option>-i / --index</option>
+ or the <option>-s / --system</option> options.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>-q</option></term>
<term><option>--quiet</option></term>
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 3cd793b134..ede665090f 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -29,7 +29,7 @@ dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+reindexdb: reindexdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
install: all installdirs
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index ca61348a0e..4c23e35338 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -10,10 +10,14 @@
*/
#include "postgres_fe.h"
+
+#include "catalog/pg_class_d.h"
#include "common.h"
#include "common/logging.h"
+#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
typedef enum ReindexType
{
@@ -25,16 +29,25 @@ typedef enum ReindexType
} ReindexType;
-static void reindex_one_database(const char *name, const char *dbname,
- ReindexType type, const char *host,
+static SimpleStringList *get_parallel_object_list(PGconn *conn,
+ const char *progname,
+ bool echo);
+static void reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
const char *port, const char *username,
enum trivalue prompt_password, const char *progname,
- bool echo, bool verbose, bool concurrently);
+ bool echo, bool verbose, bool concurrently,
+ int concurrentCons);
static void reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo,
- bool quiet, bool verbose, bool concurrently);
+ bool quiet, bool verbose, bool concurrently,
+ int concurrentCons);
+static void run_reindex_command(PGconn *conn, ReindexType type,
+ const char *name, const char *progname, bool echo,
+ bool verbose, bool concurrently, bool async);
+
static void help(const char *progname);
int
@@ -54,6 +67,7 @@ main(int argc, char *argv[])
{"system", no_argument, NULL, 's'},
{"table", required_argument, NULL, 't'},
{"index", required_argument, NULL, 'i'},
+ {"jobs", required_argument, NULL, 'j'},
{"verbose", no_argument, NULL, 'v'},
{"concurrently", no_argument, NULL, 1},
{"maintenance-db", required_argument, NULL, 2},
@@ -79,6 +93,9 @@ main(int argc, char *argv[])
SimpleStringList indexes = {NULL, NULL};
SimpleStringList tables = {NULL, NULL};
SimpleStringList schemas = {NULL, NULL};
+ int concurrentCons = 1;
+ int tbl_count = 0,
+ nsp_count = 0;
pg_logging_init(argv[0]);
progname = get_progname(argv[0]);
@@ -87,7 +104,7 @@ main(int argc, char *argv[])
handle_help_version_opts(argc, argv, "reindexdb", help);
/* process command-line options */
- while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:v", long_options, &optindex)) != -1)
+ while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:j:v", long_options, &optindex)) != -1)
{
switch (c)
{
@@ -114,6 +131,7 @@ main(int argc, char *argv[])
break;
case 'S':
simple_string_list_append(&schemas, optarg);
+ nsp_count++;
break;
case 'd':
dbname = pg_strdup(optarg);
@@ -126,10 +144,25 @@ main(int argc, char *argv[])
break;
case 't':
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
case 'i':
simple_string_list_append(&indexes, optarg);
break;
+ case 'j':
+ concurrentCons = atoi(optarg);
+ if (concurrentCons <= 0)
+ {
+ pg_log_error("number of parallel jobs must be at least 1");
+ exit(1);
+ }
+ if (concurrentCons > FD_SETSIZE - 1)
+ {
+ pg_log_error("too many parallel jobs requested (maximum: %d)",
+ FD_SETSIZE - 1);
+ exit(1);
+ }
+ break;
case 'v':
verbose = true;
break;
@@ -194,7 +227,8 @@ main(int argc, char *argv[])
}
reindex_all_databases(maintenance_db, host, port, username,
- prompt_password, progname, echo, quiet, verbose, concurrently);
+ prompt_password, progname, echo, quiet, verbose,
+ concurrently, 1);
}
else if (syscatalog)
{
@@ -214,6 +248,12 @@ main(int argc, char *argv[])
exit(1);
}
+ if (concurrentCons > 1)
+ {
+ pg_log_error("cannot use multiple jobs to reindex system catalogs");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -224,9 +264,9 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- reindex_one_database(NULL, dbname, REINDEX_SYSTEM, host,
+ reindex_one_database(dbname, REINDEX_SYSTEM, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, 1);
}
else
{
@@ -240,62 +280,59 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- if (schemas.head != NULL)
+ if (indexes.head != NULL)
{
- SimpleStringListCell *cell;
-
- for (cell = schemas.head; cell; cell = cell->next)
+ if (concurrentCons > 1)
{
- reindex_one_database(cell->val, dbname, REINDEX_SCHEMA, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ pg_log_error("cannot use multiple jobs to reindex multiple indexes");
+ exit(1);
}
+
+ reindex_one_database(dbname, REINDEX_INDEX, &indexes, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ 1);
}
- if (indexes.head != NULL)
- {
- SimpleStringListCell *cell;
+ if (schemas.head != NULL)
+ reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, nsp_count));
- for (cell = indexes.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_INDEX, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
if (tables.head != NULL)
- {
- SimpleStringListCell *cell;
-
- for (cell = tables.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_TABLE, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ reindex_one_database(dbname, REINDEX_TABLE, &tables, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, tbl_count));
/*
* reindex database only if neither index nor table nor schema is
* specified
*/
if (indexes.head == NULL && tables.head == NULL && schemas.head == NULL)
- reindex_one_database(NULL, dbname, REINDEX_DATABASE, host,
+ reindex_one_database(dbname, REINDEX_DATABASE, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, concurrentCons);
}
exit(0);
}
static void
-reindex_one_database(const char *name, const char *dbname, ReindexType type,
- const char *host, const char *port, const char *username,
+reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
+ const char *port, const char *username,
enum trivalue prompt_password, const char *progname, bool echo,
- bool verbose, bool concurrently)
+ bool verbose, bool concurrently, int concurrentCons)
{
- PQExpBufferData sql;
PGconn *conn;
+ SimpleStringListCell *cell;
+ bool parallel = concurrentCons > 1;
+ SimpleStringList *process_list = user_list;
+ ReindexType process_type = type;
+ ParallelSlot *slots;
+ bool failed = false;
conn = connectDatabase(dbname, host, port, username, prompt_password,
progname, echo, false, false);
@@ -308,6 +345,92 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
exit(1);
}
+ if (!parallel)
+ {
+ if (user_list == NULL)
+ {
+ /*
+ * Create a dummy list with an empty string, as user requires an
+ * element.
+ */
+ process_list = pg_malloc0(sizeof(SimpleStringList));
+ simple_string_list_append(process_list, "");
+ }
+ }
+ else
+ {
+ /*
+ * Database-wide parallel reindex requires special processing. If
+ * multiple jobs were asked, we have to reindex system catalogs first,
+ * as they can't be processed in parallel.
+ */
+ if (process_type == REINDEX_DATABASE)
+ {
+ Assert(user_list == NULL);
+ run_reindex_command(conn, REINDEX_SYSTEM, NULL, progname, echo,
+ verbose, concurrently, false);
+
+ process_type = REINDEX_TABLE;
+ process_list = get_parallel_object_list(conn, progname, echo);
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ }
+ else
+ Assert(user_list != NULL);
+ }
+
+ slots = ParallelSlotsSetup(dbname, host, port, username, prompt_password,
+ progname, echo, conn, concurrentCons);
+
+ cell = process_list->head;
+ do
+ {
+ const char *objname = cell->val;
+ ParallelSlot *free_slot = NULL;
+
+ if (CancelRequested)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ free_slot = ParallelSlotsGetIdle(slots, concurrentCons, progname);
+ if (!free_slot)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ run_reindex_command(conn, process_type, objname, progname, echo,
+ verbose, concurrently, true);
+
+ cell = cell->next;
+ } while (cell != NULL);
+
+ if (!ParallelSlotsWaitCompletion(slots, concurrentCons, progname))
+ failed = true;
+
+finish:
+ ParallelSlotsTerminate(slots, concurrentCons);
+ pfree(slots);
+
+ if (failed)
+ exit(1);
+}
+
+static void
+run_reindex_command(PGconn *conn, ReindexType type, const char *name,
+ const char *progname, bool echo, bool verbose,
+ bool concurrently, bool async)
+{
+ PQExpBufferData sql;
+ bool status;
+
/* build the REINDEX query */
initPQExpBuffer(&sql);
@@ -358,7 +481,17 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
/* finish the query */
appendPQExpBufferChar(&sql, ';');
- if (!executeMaintenanceCommand(conn, sql.data, echo))
+ if (async)
+ {
+ if (echo)
+ printf("%s\n", sql.data);
+
+ status = PQsendQuery(conn, sql.data) == 1;
+ }
+ else
+ status = executeMaintenanceCommand(conn, sql.data, echo);
+
+ if (!status)
{
switch (type)
{
@@ -383,20 +516,90 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
name, PQdb(conn), PQerrorMessage(conn));
break;
}
- PQfinish(conn);
- exit(1);
+ if (!async)
+ {
+ PQfinish(conn);
+ exit(1);
+ }
}
- PQfinish(conn);
termPQExpBuffer(&sql);
}
+/*
+ * Prepare the list of objects to process by querying the catalogs.
+ *
+ * This function will return a SimpleStringList object containing the entire
+ * list of tables in the given database that should be processed by a parallel
+ * database-wide reindex (excluding system tables), or NULL if there's no such
+ * table.
+ */
+static SimpleStringList *
+get_parallel_object_list(PGconn *conn, const char *progname, bool echo)
+{
+ PQExpBufferData catalog_query;
+ PQExpBufferData buf;
+ PGresult *res;
+ SimpleStringList *tables;
+ int ntups,
+ i;
+
+ initPQExpBuffer(&catalog_query);
+
+ /*
+ * This query is run using a safe search_path, so there's no need to fully
+ * qualify everything.
+ */
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE ns.nspname != 'pg_catalog'\n"
+ " AND c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " ORDER BY c.relpages DESC;");
+
+ res = executeQuery(conn, catalog_query.data, progname, echo);
+ termPQExpBuffer(&catalog_query);
+
+ /*
+ * If no rows are returned, there are no matching tables, so we are done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return NULL;
+ }
+
+ tables = pg_malloc0(sizeof(SimpleStringList));
+
+ /* Build qualified identifiers for each table */
+ initPQExpBuffer(&buf);
+ for (i = 0; i < ntups; i++)
+ {
+ appendPQExpBufferStr(&buf,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+
+ simple_string_list_append(tables, buf.data);
+ resetPQExpBuffer(&buf);
+ }
+ termPQExpBuffer(&buf);
+ PQclear(res);
+
+ return tables;
+}
+
static void
reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo, bool quiet, bool verbose,
- bool concurrently)
+ bool concurrently, int concurrentCons)
{
PGconn *conn;
PGresult *result;
@@ -423,9 +626,10 @@ reindex_all_databases(const char *maintenance_db,
appendPQExpBufferStr(&connstr, "dbname=");
appendConnStrVal(&connstr, dbname);
- reindex_one_database(NULL, connstr.data, REINDEX_DATABASE, host,
+ reindex_one_database(connstr.data, REINDEX_DATABASE, NULL, host,
port, username, prompt_password,
- progname, echo, verbose, concurrently);
+ progname, echo, verbose, concurrently,
+ concurrentCons);
}
termPQExpBuffer(&connstr);
@@ -444,6 +648,7 @@ help(const char *progname)
printf(_(" -d, --dbname=DBNAME database to reindex\n"));
printf(_(" -e, --echo show the commands being sent to the server\n"));
printf(_(" -i, --index=INDEX recreate specific index(es) only\n"));
+ printf(_(" -j, --jobs=NUM use this many concurrent connections to reindex\n"));
printf(_(" -q, --quiet don't write any messages\n"));
printf(_(" -s, --system reindex system catalogs\n"));
printf(_(" -S, --schema=SCHEMA reindex specific schema(s) only\n"));
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index 1af8ab70ad..7f52e50566 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -3,7 +3,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 34;
+use Test::More tests => 40;
program_help_ok('reindexdb');
program_version_ok('reindexdb');
@@ -77,3 +77,15 @@ $node->command_ok(
$node->command_ok(
[qw(reindexdb --echo --system dbname=template1)],
'reindexdb system with connection string');
+
+# parallel processing
+$node->command_fails([qw(reindexdb -j2 -s)],
+ 'reindexdb cannot process system catalogs in parallel');
+$node->command_fails([qw(reindexdb -j2 -i i1 -i i2)],
+ 'reindexdb cannot process indexes in parallel');
+$node->issues_sql_like([qw(reindexdb -j2)],
+ qr/statement: REINDEX SYSTEM postgres/,
+ 'Global and parallel reindex will issue a REINDEX SYSTEM');
+$node->issues_sql_like([qw(reindexdb -j2)],
+ qr/statement: REINDEX TABLE public.test1/,
+ 'Global and parallel reindex will issue per-table REINDEX');
--
2.20.1
On Fri, Jul 12, 2019 at 11:47 AM Julien Rouhaud <rjuju123@gmail.com> wrote:
I didn't change the behavior wrt. possible deadlock if user specify
catalog objects using --index or --table and ask for multiple
connection, as I'm afraid that it'll add too much code for a little
benefit. Please shout if you think otherwise.
Sorry I meant schemas, not indexes.
After more thinking about schema and multiple jobs, I think that
erroring out is quite user unfriendly, as it's entirely ok to ask for
multiple indexes and multiple object that do support parallelism in a
single call. So I think it's better to remove the error, ignore the
given --jobs options for indexes and document this behavior.
On Tue, Jul 16, 2019 at 02:03:16PM +0200, Julien Rouhaud wrote:
After more thinking about schema and multiple jobs, I think that
erroring out is quite user unfriendly, as it's entirely ok to ask
for
multiple indexes and multiple object that do support parallelism in
a
single call. So I think it's better to remove the error, ignore the
given --jobs options for indexes and document this behavior.
No objections to that. I still need to study a bit more 0002 though
to come to a clear conclusion.
Actually, from patch 0002:
+ free_slot = ParallelSlotsGetIdle(slots, concurrentCons, progname);
+ if (!free_slot)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ run_reindex_command(conn, process_type, objname, progname, echo,
+ verbose, concurrently, true);
The same connection gets reused, shouldn't the connection come from
the free slot?
On top of that quick lookup, I have done an in-depth review on 0001 to
bring it to a committable state, fixing a couple of typos, incorrect
comments (description of ParallelSlotsGetIdle was for example
incorrect) on the way. Other things include that connectDatabase
should have an assertion for a non-NULL connection, calling pg_free()
on the slots terminate is more consistent as pg_malloc is used first.
A comment at the top of processQueryResult still referred to
vacuuming of a missing relation. Most of the patch was in a clean
state, with a clear interface for parallel slots, the place of the new
routines also makes sense, so I did not have much to do :)
Another thing I have noticed is that we don't really need to pass down
progname across all those layers as we finish by using pg_log_error()
when processing results, so more simplifications can be done. Let's
handle all that in the same patch as we are messing with the area.
connectDatabase() and connectMaintenanceDatabase() still need it
though as this is used in the connection string, so
ParallelSlotsSetup() also needs it. This part is not really your
fault but as I am looking at it, it does not hurt to clean up what we
can. Attached is an updated version of 0001 that I am comfortable
with. I'd like to commit that with the cleanups included and then
let's move to the real deal with 0002.
--
Michael
Attachments:
scripts-refactor-parallel.patchtext/x-diff; charset=us-asciiDownload
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 9f352b5e2b..3cd793b134 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -28,7 +28,7 @@ createuser: createuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport
dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-vacuumdb: vacuumdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
@@ -50,7 +50,7 @@ uninstall:
clean distclean maintainer-clean:
rm -f $(addsuffix $(X), $(PROGRAMS)) $(addsuffix .o, $(PROGRAMS))
- rm -f common.o $(WIN32RES)
+ rm -f common.o scripts_parallel.o $(WIN32RES)
rm -rf tmp_check
check:
diff --git a/src/bin/scripts/clusterdb.c b/src/bin/scripts/clusterdb.c
index ae0facd5a7..d380127356 100644
--- a/src/bin/scripts/clusterdb.c
+++ b/src/bin/scripts/clusterdb.c
@@ -206,7 +206,7 @@ cluster_one_database(const char *dbname, bool verbose, const char *table,
if (table)
{
appendPQExpBufferChar(&sql, ' ');
- appendQualifiedRelation(&sql, table, conn, progname, echo);
+ appendQualifiedRelation(&sql, table, conn, echo);
}
appendPQExpBufferChar(&sql, ';');
@@ -239,7 +239,7 @@ cluster_all_databases(bool verbose, const char *maintenance_db,
conn = connectMaintenanceDatabase(maintenance_db, host, port, username,
prompt_password, progname, echo);
- result = executeQuery(conn, "SELECT datname FROM pg_database WHERE datallowconn ORDER BY 1;", progname, echo);
+ result = executeQuery(conn, "SELECT datname FROM pg_database WHERE datallowconn ORDER BY 1;", echo);
PQfinish(conn);
initPQExpBuffer(&connstr);
diff --git a/src/bin/scripts/common.c b/src/bin/scripts/common.c
index 296029d809..41a8b4891c 100644
--- a/src/bin/scripts/common.c
+++ b/src/bin/scripts/common.c
@@ -22,6 +22,8 @@
#include "fe_utils/connect.h"
#include "fe_utils/string_utils.h"
+#define ERRCODE_UNDEFINED_TABLE "42P01"
+
static PGcancel *volatile cancelConn = NULL;
bool CancelRequested = false;
@@ -146,8 +148,7 @@ connectDatabase(const char *dbname, const char *pghost,
exit(1);
}
- PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL,
- progname, echo));
+ PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo));
return conn;
}
@@ -178,11 +179,35 @@ connectMaintenanceDatabase(const char *maintenance_db,
return conn;
}
+/*
+ * Disconnect the given connection, canceling any statement if one is active.
+ */
+void
+disconnectDatabase(PGconn *conn)
+{
+ char errbuf[256];
+
+ Assert(conn != NULL);
+
+ if (PQtransactionStatus(conn) == PQTRANS_ACTIVE)
+ {
+ PGcancel *cancel;
+
+ if ((cancel = PQgetCancel(conn)))
+ {
+ (void) PQcancel(cancel, errbuf, sizeof(errbuf));
+ PQfreeCancel(cancel);
+ }
+ }
+
+ PQfinish(conn);
+}
+
/*
* Run a query, return the results, exit program on failure.
*/
PGresult *
-executeQuery(PGconn *conn, const char *query, const char *progname, bool echo)
+executeQuery(PGconn *conn, const char *query, bool echo)
{
PGresult *res;
@@ -207,8 +232,7 @@ executeQuery(PGconn *conn, const char *query, const char *progname, bool echo)
* As above for a SQL command (which returns nothing).
*/
void
-executeCommand(PGconn *conn, const char *query,
- const char *progname, bool echo)
+executeCommand(PGconn *conn, const char *query, bool echo)
{
PGresult *res;
@@ -255,6 +279,61 @@ executeMaintenanceCommand(PGconn *conn, const char *query, bool echo)
return r;
}
+/*
+ * consumeQueryResult
+ *
+ * Consume all the results generated for the given connection until
+ * nothing remains. If at least one error is encountered, return false.
+ * Note that this will block if the conn is busy.
+ */
+bool
+consumeQueryResult(PGconn *conn)
+{
+ bool ok = true;
+ PGresult *result;
+
+ SetCancelConn(conn);
+ while ((result = PQgetResult(conn)) != NULL)
+ {
+ if (!processQueryResult(conn, result))
+ ok = false;
+ }
+ ResetCancelConn();
+ return ok;
+}
+
+/*
+ * processQueryResult
+ *
+ * Process (and delete) a query result. Returns true if there's no error,
+ * false otherwise -- but errors about trying to work on a missing relation
+ * are reported and subsequently ignored.
+ */
+bool
+processQueryResult(PGconn *conn, PGresult *result)
+{
+ /*
+ * If it's an error, report it. Errors about a missing table are harmless
+ * so we continue processing; but die for other errors.
+ */
+ if (PQresultStatus(result) != PGRES_COMMAND_OK)
+ {
+ char *sqlState = PQresultErrorField(result, PG_DIAG_SQLSTATE);
+
+ pg_log_error("processing of database \"%s\" failed: %s",
+ PQdb(conn), PQerrorMessage(conn));
+
+ if (sqlState && strcmp(sqlState, ERRCODE_UNDEFINED_TABLE) != 0)
+ {
+ PQclear(result);
+ return false;
+ }
+ }
+
+ PQclear(result);
+ return true;
+}
+
/*
* Split TABLE[(COLUMNS)] into TABLE and [(COLUMNS)] portions. When you
@@ -299,7 +378,7 @@ splitTableColumnsSpec(const char *spec, int encoding,
*/
void
appendQualifiedRelation(PQExpBuffer buf, const char *spec,
- PGconn *conn, const char *progname, bool echo)
+ PGconn *conn, bool echo)
{
char *table;
const char *columns;
@@ -324,7 +403,7 @@ appendQualifiedRelation(PQExpBuffer buf, const char *spec,
appendStringLiteralConn(&sql, table, conn);
appendPQExpBufferStr(&sql, "::pg_catalog.regclass;");
- executeCommand(conn, "RESET search_path;", progname, echo);
+ executeCommand(conn, "RESET search_path;", echo);
/*
* One row is a typical result, as is a nonexistent relation ERROR.
@@ -332,7 +411,7 @@ appendQualifiedRelation(PQExpBuffer buf, const char *spec,
* relation has that OID; this query returns no rows. Catalog corruption
* might elicit other row counts.
*/
- res = executeQuery(conn, sql.data, progname, echo);
+ res = executeQuery(conn, sql.data, echo);
ntups = PQntuples(res);
if (ntups != 1)
{
@@ -351,8 +430,7 @@ appendQualifiedRelation(PQExpBuffer buf, const char *spec,
termPQExpBuffer(&sql);
pg_free(table);
- PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL,
- progname, echo));
+ PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo));
}
diff --git a/src/bin/scripts/common.h b/src/bin/scripts/common.h
index 35d1a3e0d5..f36b26a576 100644
--- a/src/bin/scripts/common.h
+++ b/src/bin/scripts/common.h
@@ -39,20 +39,24 @@ extern PGconn *connectMaintenanceDatabase(const char *maintenance_db,
const char *pguser, enum trivalue prompt_password,
const char *progname, bool echo);
-extern PGresult *executeQuery(PGconn *conn, const char *query,
- const char *progname, bool echo);
+extern void disconnectDatabase(PGconn *conn);
-extern void executeCommand(PGconn *conn, const char *query,
- const char *progname, bool echo);
+extern PGresult *executeQuery(PGconn *conn, const char *query, bool echo);
+
+extern void executeCommand(PGconn *conn, const char *query, bool echo);
extern bool executeMaintenanceCommand(PGconn *conn, const char *query,
bool echo);
+extern bool consumeQueryResult(PGconn *conn);
+
+extern bool processQueryResult(PGconn *conn, PGresult *result);
+
extern void splitTableColumnsSpec(const char *spec, int encoding,
char **table, const char **columns);
extern void appendQualifiedRelation(PQExpBuffer buf, const char *name,
- PGconn *conn, const char *progname, bool echo);
+ PGconn *conn, bool echo);
extern bool yesno_prompt(const char *question);
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index ca61348a0e..219a9a9211 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -348,7 +348,7 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
break;
case REINDEX_INDEX:
case REINDEX_TABLE:
- appendQualifiedRelation(&sql, name, conn, progname, echo);
+ appendQualifiedRelation(&sql, name, conn, echo);
break;
case REINDEX_SCHEMA:
appendPQExpBufferStr(&sql, name);
@@ -405,7 +405,7 @@ reindex_all_databases(const char *maintenance_db,
conn = connectMaintenanceDatabase(maintenance_db, host, port, username,
prompt_password, progname, echo);
- result = executeQuery(conn, "SELECT datname FROM pg_database WHERE datallowconn ORDER BY 1;", progname, echo);
+ result = executeQuery(conn, "SELECT datname FROM pg_database WHERE datallowconn ORDER BY 1;", echo);
PQfinish(conn);
initPQExpBuffer(&connstr);
diff --git a/src/bin/scripts/scripts_parallel.c b/src/bin/scripts/scripts_parallel.c
new file mode 100644
index 0000000000..1b496c9e34
--- /dev/null
+++ b/src/bin/scripts/scripts_parallel.c
@@ -0,0 +1,283 @@
+/*-------------------------------------------------------------------------
+ *
+ * scripts_parallel.c
+ * Parallel support for bin/scripts/
+ *
+ *
+ * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/bin/scripts/scripts_parallel.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#ifdef HAVE_SYS_SELECT_H
+#include <sys/select.h>
+#endif
+
+#include "common.h"
+#include "common/logging.h"
+#include "scripts_parallel.h"
+
+static void init_slot(ParallelSlot *slot, PGconn *conn);
+static int select_loop(int maxFd, fd_set *workerset, bool *aborting);
+
+static void
+init_slot(ParallelSlot *slot, PGconn *conn)
+{
+ slot->connection = conn;
+ /* Initially assume connection is idle */
+ slot->isFree = true;
+}
+
+/*
+ * Loop on select() until a descriptor from the given set becomes readable.
+ *
+ * If we get a cancel request while we're waiting, we forego all further
+ * processing and set the *aborting flag to true. The return value must be
+ * ignored in this case. Otherwise, *aborting is set to false.
+ */
+static int
+select_loop(int maxFd, fd_set *workerset, bool *aborting)
+{
+ int i;
+ fd_set saveSet = *workerset;
+
+ if (CancelRequested)
+ {
+ *aborting = true;
+ return -1;
+ }
+ else
+ *aborting = false;
+
+ for (;;)
+ {
+ /*
+ * On Windows, we need to check once in a while for cancel requests;
+ * on other platforms we rely on select() returning when interrupted.
+ */
+ struct timeval *tvp;
+#ifdef WIN32
+ struct timeval tv = {0, 1000000};
+
+ tvp = &tv;
+#else
+ tvp = NULL;
+#endif
+
+ *workerset = saveSet;
+ i = select(maxFd + 1, workerset, NULL, NULL, tvp);
+
+#ifdef WIN32
+ if (i == SOCKET_ERROR)
+ {
+ i = -1;
+
+ if (WSAGetLastError() == WSAEINTR)
+ errno = EINTR;
+ }
+#endif
+
+ if (i < 0 && errno == EINTR)
+ continue; /* ignore this */
+ if (i < 0 || CancelRequested)
+ *aborting = true; /* but not this */
+ if (i == 0)
+ continue; /* timeout (Win32 only) */
+ break;
+ }
+
+ return i;
+}
+
+/*
+ * ParallelSlotsGetIdle
+ * Return a connection slot that is ready to execute a command.
+ *
+ * This returns the first slot we find that is marked isFree, if one is;
+ * otherwise, we loop on select() until one socket becomes available. When
+ * this happens, we read the whole set and mark as free all sockets that
+ * become available. If an error occurs, NULL is returned.
+ */
+ParallelSlot *
+ParallelSlotsGetIdle(ParallelSlot *slots, int numslots)
+{
+ int i;
+ int firstFree = -1;
+
+ /*
+ * Look for any connection currently free. If there is one, mark it as
+ * taken and let the caller know the slot to use.
+ */
+ for (i = 0; i < numslots; i++)
+ {
+ if (slots[i].isFree)
+ {
+ slots[i].isFree = false;
+ return slots + i;
+ }
+ }
+
+ /*
+ * No free slot found, so wait until one of the connections has finished
+ * its task and return the available slot.
+ */
+ while (firstFree < 0)
+ {
+ fd_set slotset;
+ int maxFd = 0;
+ bool aborting;
+
+ /* We must reconstruct the fd_set for each call to select_loop */
+ FD_ZERO(&slotset);
+
+ for (i = 0; i < numslots; i++)
+ {
+ int sock = PQsocket(slots[i].connection);
+
+ /*
+ * We don't really expect any connections to lose their sockets
+ * after startup, but just in case, cope by ignoring them.
+ */
+ if (sock < 0)
+ continue;
+
+ FD_SET(sock, &slotset);
+ if (sock > maxFd)
+ maxFd = sock;
+ }
+
+ SetCancelConn(slots->connection);
+ i = select_loop(maxFd, &slotset, &aborting);
+ ResetCancelConn();
+
+ if (aborting)
+ {
+ /*
+ * We set the cancel-receiving connection to the one in the zeroth
+ * slot above, so fetch the error from there.
+ */
+ consumeQueryResult(slots->connection);
+ return NULL;
+ }
+ Assert(i != 0);
+
+ for (i = 0; i < numslots; i++)
+ {
+ int sock = PQsocket(slots[i].connection);
+
+ if (sock >= 0 && FD_ISSET(sock, &slotset))
+ {
+ /* select() says input is available, so consume it */
+ PQconsumeInput(slots[i].connection);
+ }
+
+ /* Collect result(s) as long as any are available */
+ while (!PQisBusy(slots[i].connection))
+ {
+ PGresult *result = PQgetResult(slots[i].connection);
+
+ if (result != NULL)
+ {
+ /* Check and discard the command result */
+ if (!processQueryResult(slots[i].connection, result))
+ return NULL;
+ }
+ else
+ {
+ /* This connection has become idle */
+ slots[i].isFree = true;
+ if (firstFree < 0)
+ firstFree = i;
+ break;
+ }
+ }
+ }
+ }
+
+ slots[firstFree].isFree = false;
+ return slots + firstFree;
+}
+
+/*
+ * ParallelSlotsSetup
+ * Prepare a set of parallel slots to use on a given database.
+ *
+ * This creates and initializes a set of connections to the database
+ * using the information given by the caller, marking all parallel slots
+ * as free and ready to use. "conn" is an initial connection set up
+ * by the caller and is associated with the first slot in the parallel
+ * set.
+ */
+ParallelSlot *
+ParallelSlotsSetup(const char *dbname, const char *host, const char *port,
+ const char *username, bool prompt_password,
+ const char *progname, bool echo,
+ PGconn *conn, int numslots)
+{
+ ParallelSlot *slots;
+ int i;
+
+ slots = (ParallelSlot *) pg_malloc(sizeof(ParallelSlot) * numslots);
+ init_slot(slots, conn);
+ if (numslots > 1)
+ {
+ for (i = 1; i < numslots; i++)
+ {
+ conn = connectDatabase(dbname, host, port, username, prompt_password,
+ progname, echo, false, true);
+ init_slot(slots + i, conn);
+ }
+ }
+
+ return slots;
+}
+
+/*
+ * ParallelSlotsTerminate
+ * Clean up a set of parallel slots
+ *
+ * Iterate through all connections in a given set of ParallelSlots and
+ * terminate all connections.
+ */
+void
+ParallelSlotsTerminate(ParallelSlot *slots, int numslots)
+{
+ int i;
+
+ for (i = 0; i < numslots; i++)
+ {
+ PGconn *conn = slots[i].connection;
+
+ if (conn == NULL)
+ continue;
+
+ disconnectDatabase(conn);
+ }
+
+ pg_free(slots);
+}
+
+/*
+ * ParallelSlotsWaitCompletion
+ *
+ * Wait for all connections to finish, returning false if at least one
+ * error has been found on the way.
+ */
+bool
+ParallelSlotsWaitCompletion(ParallelSlot *slots, int numslots)
+{
+ int i;
+
+ for (i = 0; i < numslots; i++)
+ {
+ if (!consumeQueryResult((slots + i)->connection))
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/bin/scripts/scripts_parallel.h b/src/bin/scripts/scripts_parallel.h
new file mode 100644
index 0000000000..f7a4c913a3
--- /dev/null
+++ b/src/bin/scripts/scripts_parallel.h
@@ -0,0 +1,36 @@
+/*-------------------------------------------------------------------------
+ *
+ * scripts_parallel.h
+ * Parallel support for bin/scripts/
+ *
+ * Copyright (c) 2003-2019, PostgreSQL Global Development Group
+ *
+ * src/bin/scripts/scripts_parallel.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SCRIPTS_PARALLEL_H
+#define SCRIPTS_PARALLEL_H
+
+/* Parallel processing stuff */
+typedef struct ParallelSlot
+{
+ PGconn *connection; /* One connection */
+ bool isFree; /* Is it known to be idle? */
+} ParallelSlot;
+
+extern ParallelSlot *ParallelSlotsGetIdle(ParallelSlot *slots, int numslots);
+
+extern ParallelSlot *ParallelSlotsSetup(const char *dbname, const char *host,
+ const char *port,
+ const char *username,
+ bool prompt_password,
+ const char *progname, bool echo,
+ PGconn *conn, int numslots);
+
+extern void ParallelSlotsTerminate(ParallelSlot *slots, int numslots);
+
+extern bool ParallelSlotsWaitCompletion(ParallelSlot *slots, int numslots);
+
+
+#endif /* SCRIPTS_PARALLEL_H */
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 3bcd14b4dc..89f84dc4c5 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -12,10 +12,6 @@
#include "postgres_fe.h"
-#ifdef HAVE_SYS_SELECT_H
-#include <sys/select.h>
-#endif
-
#include "catalog/pg_class_d.h"
#include "common.h"
@@ -23,17 +19,9 @@
#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
-#define ERRCODE_UNDEFINED_TABLE "42P01"
-
-/* Parallel vacuuming stuff */
-typedef struct ParallelSlot
-{
- PGconn *connection; /* One connection */
- bool isFree; /* Is it known to be idle? */
-} ParallelSlot;
-
/* vacuum options controlled by user flags */
typedef struct vacuumingOptions
{
@@ -69,21 +57,7 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
vacuumingOptions *vacopts, const char *table);
static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
- const char *table, const char *progname, bool async);
-
-static ParallelSlot *GetIdleSlot(ParallelSlot slots[], int numslots,
- const char *progname);
-
-static bool ProcessQueryResult(PGconn *conn, PGresult *result,
- const char *progname);
-
-static bool GetQueryResult(PGconn *conn, const char *progname);
-
-static void DisconnectDatabase(ParallelSlot *slot);
-
-static int select_loop(int maxFd, fd_set *workerset, bool *aborting);
-
-static void init_slot(ParallelSlot *slot, PGconn *conn);
+ const char *table, const char *progname);
static void help(const char *progname);
@@ -569,11 +543,10 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
* query for consistency with table lookups done elsewhere by the user.
*/
appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;");
- executeCommand(conn, "RESET search_path;", progname, echo);
- res = executeQuery(conn, catalog_query.data, progname, echo);
+ executeCommand(conn, "RESET search_path;", echo);
+ res = executeQuery(conn, catalog_query.data, echo);
termPQExpBuffer(&catalog_query);
- PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL,
- progname, echo));
+ PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo));
/*
* If no rows are returned, there are no matching tables, so we are done.
@@ -625,17 +598,9 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
*/
if (concurrentCons <= 0)
concurrentCons = 1;
- slots = (ParallelSlot *) pg_malloc(sizeof(ParallelSlot) * concurrentCons);
- init_slot(slots, conn);
- if (parallel)
- {
- for (i = 1; i < concurrentCons; i++)
- {
- conn = connectDatabase(dbname, host, port, username, prompt_password,
- progname, echo, false, true);
- init_slot(slots + i, conn);
- }
- }
+
+ slots = ParallelSlotsSetup(dbname, host, port, username, prompt_password,
+ progname, echo, conn, concurrentCons);
/*
* Prepare all the connections to run the appropriate analyze stage, if
@@ -649,7 +614,7 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
for (j = 0; j < concurrentCons; j++)
executeCommand((slots + j)->connection,
- stage_commands[stage], progname, echo);
+ stage_commands[stage], echo);
}
initPQExpBuffer(&sql);
@@ -666,63 +631,31 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
goto finish;
}
- /*
- * Get the connection slot to use. If in parallel mode, here we wait
- * for one connection to become available if none already is. In
- * non-parallel mode we simply use the only slot we have, which we
- * know to be free.
- */
- if (parallel)
+ free_slot = ParallelSlotsGetIdle(slots, concurrentCons);
+ if (!free_slot)
{
- /*
- * Get a free slot, waiting until one becomes free if none
- * currently is.
- */
- free_slot = GetIdleSlot(slots, concurrentCons, progname);
- if (!free_slot)
- {
- failed = true;
- goto finish;
- }
-
- free_slot->isFree = false;
+ failed = true;
+ goto finish;
}
- else
- free_slot = slots;
prepare_vacuum_command(&sql, PQserverVersion(free_slot->connection),
vacopts, tabname);
/*
- * Execute the vacuum. If not in parallel mode, this terminates the
- * program in case of an error. (The parallel case handles query
- * errors in ProcessQueryResult through GetIdleSlot.)
+ * Execute the vacuum. All errors are handled in processQueryResult
+ * through ParallelSlotsGetIdle.
*/
run_vacuum_command(free_slot->connection, sql.data,
- echo, tabname, progname, parallel);
+ echo, tabname, progname);
cell = cell->next;
} while (cell != NULL);
- if (parallel)
- {
- int j;
-
- /* wait for all connections to finish */
- for (j = 0; j < concurrentCons; j++)
- {
- if (!GetQueryResult((slots + j)->connection, progname))
- {
- failed = true;
- goto finish;
- }
- }
- }
+ if (!ParallelSlotsWaitCompletion(slots, concurrentCons))
+ failed = true;
finish:
- for (i = 0; i < concurrentCons; i++)
- DisconnectDatabase(slots + i);
- pfree(slots);
+ ParallelSlotsTerminate(slots, concurrentCons);
termPQExpBuffer(&sql);
@@ -756,7 +689,7 @@ vacuum_all_databases(vacuumingOptions *vacopts,
prompt_password, progname, echo);
result = executeQuery(conn,
"SELECT datname FROM pg_database WHERE datallowconn ORDER BY 1;",
- progname, echo);
+ echo);
PQfinish(conn);
initPQExpBuffer(&connstr);
@@ -914,27 +847,21 @@ prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
}
/*
- * Send a vacuum/analyze command to the server. In async mode, return after
- * sending the command; else, wait for it to finish.
+ * Send a vacuum/analyze command to the server, returning after sending the
+ * command.
*
- * Any errors during command execution are reported to stderr. If async is
- * false, this function exits the program after reporting the error.
+ * Any errors during command execution are reported to stderr.
*/
static void
run_vacuum_command(PGconn *conn, const char *sql, bool echo,
- const char *table, const char *progname, bool async)
+ const char *table, const char *progname)
{
bool status;
- if (async)
- {
- if (echo)
- printf("%s\n", sql);
+ if (echo)
+ printf("%s\n", sql);
- status = PQsendQuery(conn, sql) == 1;
- }
- else
- status = executeMaintenanceCommand(conn, sql, echo);
+ status = PQsendQuery(conn, sql) == 1;
if (!status)
{
@@ -944,271 +871,9 @@ run_vacuum_command(PGconn *conn, const char *sql, bool echo,
else
pg_log_error("vacuuming of database \"%s\" failed: %s",
PQdb(conn), PQerrorMessage(conn));
-
- if (!async)
- {
- PQfinish(conn);
- exit(1);
- }
}
}
-/*
- * GetIdleSlot
- * Return a connection slot that is ready to execute a command.
- *
- * We return the first slot we find that is marked isFree, if one is;
- * otherwise, we loop on select() until one socket becomes available. When
- * this happens, we read the whole set and mark as free all sockets that become
- * available.
- *
- * If an error occurs, NULL is returned.
- */
-static ParallelSlot *
-GetIdleSlot(ParallelSlot slots[], int numslots,
- const char *progname)
-{
- int i;
- int firstFree = -1;
-
- /* Any connection already known free? */
- for (i = 0; i < numslots; i++)
- {
- if (slots[i].isFree)
- return slots + i;
- }
-
- /*
- * No free slot found, so wait until one of the connections has finished
- * its task and return the available slot.
- */
- while (firstFree < 0)
- {
- fd_set slotset;
- int maxFd = 0;
- bool aborting;
-
- /* We must reconstruct the fd_set for each call to select_loop */
- FD_ZERO(&slotset);
-
- for (i = 0; i < numslots; i++)
- {
- int sock = PQsocket(slots[i].connection);
-
- /*
- * We don't really expect any connections to lose their sockets
- * after startup, but just in case, cope by ignoring them.
- */
- if (sock < 0)
- continue;
-
- FD_SET(sock, &slotset);
- if (sock > maxFd)
- maxFd = sock;
- }
-
- SetCancelConn(slots->connection);
- i = select_loop(maxFd, &slotset, &aborting);
- ResetCancelConn();
-
- if (aborting)
- {
- /*
- * We set the cancel-receiving connection to the one in the zeroth
- * slot above, so fetch the error from there.
- */
- GetQueryResult(slots->connection, progname);
- return NULL;
- }
- Assert(i != 0);
-
- for (i = 0; i < numslots; i++)
- {
- int sock = PQsocket(slots[i].connection);
-
- if (sock >= 0 && FD_ISSET(sock, &slotset))
- {
- /* select() says input is available, so consume it */
- PQconsumeInput(slots[i].connection);
- }
-
- /* Collect result(s) as long as any are available */
- while (!PQisBusy(slots[i].connection))
- {
- PGresult *result = PQgetResult(slots[i].connection);
-
- if (result != NULL)
- {
- /* Check and discard the command result */
- if (!ProcessQueryResult(slots[i].connection, result,
- progname))
- return NULL;
- }
- else
- {
- /* This connection has become idle */
- slots[i].isFree = true;
- if (firstFree < 0)
- firstFree = i;
- break;
- }
- }
- }
- }
-
- return slots + firstFree;
-}
-
-/*
- * ProcessQueryResult
- *
- * Process (and delete) a query result. Returns true if there's no error,
- * false otherwise -- but errors about trying to vacuum a missing relation
- * are reported and subsequently ignored.
- */
-static bool
-ProcessQueryResult(PGconn *conn, PGresult *result, const char *progname)
-{
- /*
- * If it's an error, report it. Errors about a missing table are harmless
- * so we continue processing; but die for other errors.
- */
- if (PQresultStatus(result) != PGRES_COMMAND_OK)
- {
- char *sqlState = PQresultErrorField(result, PG_DIAG_SQLSTATE);
-
- pg_log_error("vacuuming of database \"%s\" failed: %s",
- PQdb(conn), PQerrorMessage(conn));
-
- if (sqlState && strcmp(sqlState, ERRCODE_UNDEFINED_TABLE) != 0)
- {
- PQclear(result);
- return false;
- }
- }
-
- PQclear(result);
- return true;
-}
-
-/*
- * GetQueryResult
- *
- * Pump the conn till it's dry of results; return false if any are errors.
- * Note that this will block if the conn is busy.
- */
-static bool
-GetQueryResult(PGconn *conn, const char *progname)
-{
- bool ok = true;
- PGresult *result;
-
- SetCancelConn(conn);
- while ((result = PQgetResult(conn)) != NULL)
- {
- if (!ProcessQueryResult(conn, result, progname))
- ok = false;
- }
- ResetCancelConn();
- return ok;
-}
-
-/*
- * DisconnectDatabase
- * Disconnect the connection associated with the given slot
- */
-static void
-DisconnectDatabase(ParallelSlot *slot)
-{
- char errbuf[256];
-
- if (!slot->connection)
- return;
-
- if (PQtransactionStatus(slot->connection) == PQTRANS_ACTIVE)
- {
- PGcancel *cancel;
-
- if ((cancel = PQgetCancel(slot->connection)))
- {
- (void) PQcancel(cancel, errbuf, sizeof(errbuf));
- PQfreeCancel(cancel);
- }
- }
-
- PQfinish(slot->connection);
- slot->connection = NULL;
-}
-
-/*
- * Loop on select() until a descriptor from the given set becomes readable.
- *
- * If we get a cancel request while we're waiting, we forego all further
- * processing and set the *aborting flag to true. The return value must be
- * ignored in this case. Otherwise, *aborting is set to false.
- */
-static int
-select_loop(int maxFd, fd_set *workerset, bool *aborting)
-{
- int i;
- fd_set saveSet = *workerset;
-
- if (CancelRequested)
- {
- *aborting = true;
- return -1;
- }
- else
- *aborting = false;
-
- for (;;)
- {
- /*
- * On Windows, we need to check once in a while for cancel requests;
- * on other platforms we rely on select() returning when interrupted.
- */
- struct timeval *tvp;
-#ifdef WIN32
- struct timeval tv = {0, 1000000};
-
- tvp = &tv;
-#else
- tvp = NULL;
-#endif
-
- *workerset = saveSet;
- i = select(maxFd + 1, workerset, NULL, NULL, tvp);
-
-#ifdef WIN32
- if (i == SOCKET_ERROR)
- {
- i = -1;
-
- if (WSAGetLastError() == WSAEINTR)
- errno = EINTR;
- }
-#endif
-
- if (i < 0 && errno == EINTR)
- continue; /* ignore this */
- if (i < 0 || CancelRequested)
- *aborting = true; /* but not this */
- if (i == 0)
- continue; /* timeout (Win32 only) */
- break;
- }
-
- return i;
-}
-
-static void
-init_slot(ParallelSlot *slot, PGconn *conn)
-{
- slot->connection = conn;
- /* Initially assume connection is idle */
- slot->isFree = true;
-}
-
static void
help(const char *progname)
{
On Wed, Jul 17, 2019 at 9:59 AM Michael Paquier <michael@paquier.xyz> wrote:
On Tue, Jul 16, 2019 at 02:03:16PM +0200, Julien Rouhaud wrote:
After more thinking about schema and multiple jobs, I think that
erroring out is quite user unfriendly, as it's entirely ok to ask
for
multiple indexes and multiple object that do support parallelism in
a
single call. So I think it's better to remove the error, ignore the
given --jobs options for indexes and document this behavior.No objections to that. I still need to study a bit more 0002 though
to come to a clear conclusion.Actually, from patch 0002: + free_slot = ParallelSlotsGetIdle(slots, concurrentCons, progname); + if (!free_slot) + { + failed = true; + goto finish; + } + + run_reindex_command(conn, process_type, objname, progname, echo, + verbose, concurrently, true); The same connection gets reused, shouldn't the connection come from the free slot?
Ouch indeed.
On top of that quick lookup, I have done an in-depth review on 0001 to
bring it to a committable state, fixing a couple of typos, incorrect
comments (description of ParallelSlotsGetIdle was for example
incorrect) on the way. Other things include that connectDatabase
should have an assertion for a non-NULL connection,
disconnectDatabase you mean? Fine by me.
calling pg_free()
on the slots terminate is more consistent as pg_malloc is used first.
A comment at the top of processQueryResult still referred to
vacuuming of a missing relation. Most of the patch was in a clean
state, with a clear interface for parallel slots, the place of the new
routines also makes sense, so I did not have much to do :)
Thanks :)
Another thing I have noticed is that we don't really need to pass down
progname across all those layers as we finish by using pg_log_error()
when processing results, so more simplifications can be done. Let's
handle all that in the same patch as we are messing with the area.
connectDatabase() and connectMaintenanceDatabase() still need it
though as this is used in the connection string, so
ParallelSlotsSetup() also needs it. This part is not really your
fault but as I am looking at it, it does not hurt to clean up what we
can. Attached is an updated version of 0001 that I am comfortable
with. I'd like to commit that with the cleanups included and then
let's move to the real deal with 0002.
Good catch, I totally missed this progname change. I read the patch
you attached, I have a few comments:
+/*
+ * Disconnect the given connection, canceling any statement if one is active.
+ */
+void
+disconnectDatabase(PGconn *conn)
Nitpicking, but this comment doesn't follow the style of other
functions' comments (it's also the case for existing comment on
executeQuery at least).
While reading the comments you added on ParallelSlotsSetup(), I
wondered if we couldn't also add an Assert(conn) at the beginning?
+void
+ParallelSlotsTerminate(ParallelSlot *slots, int numslots)
+{
+ int i;
+
+ for (i = 0; i < numslots; i++)
+ {
+ PGconn *conn = slots[i].connection;
+
+ if (conn == NULL)
+ continue;
+
+ disconnectDatabase(conn);
+ }
+
+ pg_free(slots);
+}
Is it ok to call pg_free(slots) and let caller have a pointer pointing
to freed memory?
On Wed, Jul 17, 2019 at 07:46:10PM +0200, Julien Rouhaud wrote:
On Wed, Jul 17, 2019 at 9:59 AM Michael Paquier <michael@paquier.xyz> wrote:
On top of that quick lookup, I have done an in-depth review on 0001 to
bring it to a committable state, fixing a couple of typos, incorrect
comments (description of ParallelSlotsGetIdle was for example
incorrect) on the way. Other things include that connectDatabase
should have an assertion for a non-NULL connection,disconnectDatabase you mean? Fine by me.
Oops, yes. I meant disconnectDatabase() here. The patch does so, not
my words.
+/* + * Disconnect the given connection, canceling any statement if one is active. + */ +void +disconnectDatabase(PGconn *conn)Nitpicking, but this comment doesn't follow the style of other
functions' comments (it's also the case for existing comment on
executeQuery at least).
connectDatabase, connectMaintenanceDatabase, executeQuery and most of
the others follow that style, so I am just going to simplify
consumeQueryResult and processQueryResult to keep a consistent style.
While reading the comments you added on ParallelSlotsSetup(), I
wondered if we couldn't also add an Assert(conn) at the beginning?
That makes sense as this gets associated to the first slot. There
could be an argument for making a set of slots extensible to simplify
this interface, but that complicates the logic for the scripts.
Is it ok to call pg_free(slots) and let caller have a pointer pointing
to freed memory?
The interface has a Setup call which initializes the whole thing, and
Terminate is the logical end point, so having the free logic within
the termination looks more consistent to me. We could now have actual
Init() and Free() but I am not sure that this justifies the move as
this complicates the scripts using it.
--
Michael
On Thu, Jul 18, 2019 at 09:45:14AM +0900, Michael Paquier wrote:
On Wed, Jul 17, 2019 at 07:46:10PM +0200, Julien Rouhaud wrote:
Is it ok to call pg_free(slots) and let caller have a pointer pointing
to freed memory?
The interface has a Setup call which initializes the whole thing, and
Terminate is the logical end point, so having the free logic within
the termination looks more consistent to me. We could now have actual
Init() and Free() but I am not sure that this justifies the move as
this complicates the scripts using it.
I have reconsidered this point, moved the pg_free() call out of the
termination logic, and committed the first patch after an extra lookup
and more polishing.
For the second patch, could you send a rebase with a fix for the
connection slot when processing the reindex commands?
--
Michael
On Fri, Jul 19, 2019 at 2:35 AM Michael Paquier <michael@paquier.xyz> wrote:
On Thu, Jul 18, 2019 at 09:45:14AM +0900, Michael Paquier wrote:
On Wed, Jul 17, 2019 at 07:46:10PM +0200, Julien Rouhaud wrote:
Is it ok to call pg_free(slots) and let caller have a pointer pointing
to freed memory?
The interface has a Setup call which initializes the whole thing, and
Terminate is the logical end point, so having the free logic within
the termination looks more consistent to me. We could now have actual
Init() and Free() but I am not sure that this justifies the move as
this complicates the scripts using it.I have reconsidered this point, moved the pg_free() call out of the
termination logic, and committed the first patch after an extra lookup
and more polishing.
Thanks!
For the second patch, could you send a rebase with a fix for the
connection slot when processing the reindex commands?
Attached, I also hopefully removed all the now unneeded progname usage.
Attachments:
reindex_parallel_v5.diffapplication/octet-stream; name=reindex_parallel_v5.diffDownload
diff --git a/doc/src/sgml/ref/reindexdb.sgml b/doc/src/sgml/ref/reindexdb.sgml
index 25b5a72770..a7031030b9 100644
--- a/doc/src/sgml/ref/reindexdb.sgml
+++ b/doc/src/sgml/ref/reindexdb.sgml
@@ -166,6 +166,29 @@ PostgreSQL documentation
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>-j <replaceable class="parameter">njobs</replaceable></option></term>
+ <term><option>--jobs=<replaceable class="parameter">njobs</replaceable></option></term>
+ <listitem>
+ <para>
+ Execute the reindex commands in parallel by running
+ <replaceable class="parameter">njobs</replaceable>
+ commands simultaneously. This option reduces the time of the
+ processing but it also increases the load on the database server.
+ </para>
+ <para>
+ <application>reindexdb</application> will open
+ <replaceable class="parameter">njobs</replaceable> connections to the
+ database, so make sure your <xref linkend="guc-max-connections"/>
+ setting is high enough to accommodate all connections.
+ </para>
+ <para>
+ Note that this mode is not compatible the <option>-i / --index</option>
+ or the <option>-s / --system</option> options.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>-q</option></term>
<term><option>--quiet</option></term>
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 3cd793b134..ede665090f 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -29,7 +29,7 @@ dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+reindexdb: reindexdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
install: all installdirs
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index 219a9a9211..d5ccb54a78 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -10,10 +10,14 @@
*/
#include "postgres_fe.h"
+
+#include "catalog/pg_class_d.h"
#include "common.h"
#include "common/logging.h"
+#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
typedef enum ReindexType
{
@@ -25,16 +29,23 @@ typedef enum ReindexType
} ReindexType;
-static void reindex_one_database(const char *name, const char *dbname,
- ReindexType type, const char *host,
+static SimpleStringList *get_parallel_object_list(PGconn *conn, bool echo);
+static void reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
const char *port, const char *username,
enum trivalue prompt_password, const char *progname,
- bool echo, bool verbose, bool concurrently);
+ bool echo, bool verbose, bool concurrently,
+ int concurrentCons);
static void reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo,
- bool quiet, bool verbose, bool concurrently);
+ bool quiet, bool verbose, bool concurrently,
+ int concurrentCons);
+static void run_reindex_command(PGconn *conn, ReindexType type,
+ const char *name, bool echo, bool verbose,
+ bool concurrently, bool async);
+
static void help(const char *progname);
int
@@ -54,6 +65,7 @@ main(int argc, char *argv[])
{"system", no_argument, NULL, 's'},
{"table", required_argument, NULL, 't'},
{"index", required_argument, NULL, 'i'},
+ {"jobs", required_argument, NULL, 'j'},
{"verbose", no_argument, NULL, 'v'},
{"concurrently", no_argument, NULL, 1},
{"maintenance-db", required_argument, NULL, 2},
@@ -79,6 +91,9 @@ main(int argc, char *argv[])
SimpleStringList indexes = {NULL, NULL};
SimpleStringList tables = {NULL, NULL};
SimpleStringList schemas = {NULL, NULL};
+ int concurrentCons = 1;
+ int tbl_count = 0,
+ nsp_count = 0;
pg_logging_init(argv[0]);
progname = get_progname(argv[0]);
@@ -87,7 +102,7 @@ main(int argc, char *argv[])
handle_help_version_opts(argc, argv, "reindexdb", help);
/* process command-line options */
- while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:v", long_options, &optindex)) != -1)
+ while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:j:v", long_options, &optindex)) != -1)
{
switch (c)
{
@@ -114,6 +129,7 @@ main(int argc, char *argv[])
break;
case 'S':
simple_string_list_append(&schemas, optarg);
+ nsp_count++;
break;
case 'd':
dbname = pg_strdup(optarg);
@@ -126,10 +142,25 @@ main(int argc, char *argv[])
break;
case 't':
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
case 'i':
simple_string_list_append(&indexes, optarg);
break;
+ case 'j':
+ concurrentCons = atoi(optarg);
+ if (concurrentCons <= 0)
+ {
+ pg_log_error("number of parallel jobs must be at least 1");
+ exit(1);
+ }
+ if (concurrentCons > FD_SETSIZE - 1)
+ {
+ pg_log_error("too many parallel jobs requested (maximum: %d)",
+ FD_SETSIZE - 1);
+ exit(1);
+ }
+ break;
case 'v':
verbose = true;
break;
@@ -194,7 +225,8 @@ main(int argc, char *argv[])
}
reindex_all_databases(maintenance_db, host, port, username,
- prompt_password, progname, echo, quiet, verbose, concurrently);
+ prompt_password, progname, echo, quiet, verbose,
+ concurrently, concurrentCons);
}
else if (syscatalog)
{
@@ -214,6 +246,12 @@ main(int argc, char *argv[])
exit(1);
}
+ if (concurrentCons > 1)
+ {
+ pg_log_error("cannot use multiple jobs to reindex system catalogs");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -224,9 +262,9 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- reindex_one_database(NULL, dbname, REINDEX_SYSTEM, host,
+ reindex_one_database(dbname, REINDEX_SYSTEM, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, 1);
}
else
{
@@ -241,61 +279,55 @@ main(int argc, char *argv[])
}
if (schemas.head != NULL)
- {
- SimpleStringListCell *cell;
-
- for (cell = schemas.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_SCHEMA, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, nsp_count));
if (indexes.head != NULL)
- {
- SimpleStringListCell *cell;
- for (cell = indexes.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_INDEX, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
- if (tables.head != NULL)
- {
- SimpleStringListCell *cell;
+ /*
+ * An index list cannot be processed by multiple connections, as
+ * it would require to distribute the work by owner tables. We
+ * simply ignore the passed number of jobs if any.
+ */
+ reindex_one_database(dbname, REINDEX_INDEX, &indexes, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently, 1);
- for (cell = tables.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_TABLE, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ if (tables.head != NULL)
+ reindex_one_database(dbname, REINDEX_TABLE, &tables, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, tbl_count));
/*
* reindex database only if neither index nor table nor schema is
* specified
*/
if (indexes.head == NULL && tables.head == NULL && schemas.head == NULL)
- reindex_one_database(NULL, dbname, REINDEX_DATABASE, host,
+ reindex_one_database(dbname, REINDEX_DATABASE, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, concurrentCons);
}
exit(0);
}
static void
-reindex_one_database(const char *name, const char *dbname, ReindexType type,
- const char *host, const char *port, const char *username,
+reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
+ const char *port, const char *username,
enum trivalue prompt_password, const char *progname, bool echo,
- bool verbose, bool concurrently)
+ bool verbose, bool concurrently, int concurrentCons)
{
- PQExpBufferData sql;
PGconn *conn;
+ SimpleStringListCell *cell;
+ bool parallel = concurrentCons > 1;
+ SimpleStringList *process_list = user_list;
+ ReindexType process_type = type;
+ ParallelSlot *slots;
+ bool failed = false;
conn = connectDatabase(dbname, host, port, username, prompt_password,
progname, echo, false, false);
@@ -308,6 +340,91 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
exit(1);
}
+ if (!parallel)
+ {
+ if (user_list == NULL)
+ {
+ /*
+ * Create a dummy list with an empty string, as user requires an
+ * element.
+ */
+ process_list = pg_malloc0(sizeof(SimpleStringList));
+ simple_string_list_append(process_list, "");
+ }
+ }
+ else
+ {
+ /*
+ * Database-wide parallel reindex requires special processing. If
+ * multiple jobs were asked, we have to reindex system catalogs first,
+ * as they can't be processed in parallel.
+ */
+ if (process_type == REINDEX_DATABASE)
+ {
+ Assert(user_list == NULL);
+ run_reindex_command(conn, REINDEX_SYSTEM, NULL, echo, verbose,
+ concurrently, false);
+
+ process_type = REINDEX_TABLE;
+ process_list = get_parallel_object_list(conn, echo);
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ }
+ else
+ Assert(user_list != NULL);
+ }
+
+ slots = ParallelSlotsSetup(dbname, host, port, username, prompt_password,
+ progname, echo, conn, concurrentCons);
+
+ cell = process_list->head;
+ do
+ {
+ const char *objname = cell->val;
+ ParallelSlot *free_slot = NULL;
+
+ if (CancelRequested)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ free_slot = ParallelSlotsGetIdle(slots, concurrentCons);
+ if (!free_slot)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ run_reindex_command(free_slot->connection, process_type, objname,
+ echo, verbose, concurrently, true);
+
+ cell = cell->next;
+ } while (cell != NULL);
+
+ if (!ParallelSlotsWaitCompletion(slots, concurrentCons))
+ failed = true;
+
+finish:
+ ParallelSlotsTerminate(slots, concurrentCons);
+ pfree(slots);
+
+ if (failed)
+ exit(1);
+}
+
+static void
+run_reindex_command(PGconn *conn, ReindexType type, const char *name,
+ bool echo, bool verbose, bool concurrently, bool async)
+{
+ PQExpBufferData sql;
+ bool status;
+
/* build the REINDEX query */
initPQExpBuffer(&sql);
@@ -358,7 +475,17 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
/* finish the query */
appendPQExpBufferChar(&sql, ';');
- if (!executeMaintenanceCommand(conn, sql.data, echo))
+ if (async)
+ {
+ if (echo)
+ printf("%s\n", sql.data);
+
+ status = PQsendQuery(conn, sql.data) == 1;
+ }
+ else
+ status = executeMaintenanceCommand(conn, sql.data, echo);
+
+ if (!status)
{
switch (type)
{
@@ -383,20 +510,90 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
name, PQdb(conn), PQerrorMessage(conn));
break;
}
- PQfinish(conn);
- exit(1);
+ if (!async)
+ {
+ PQfinish(conn);
+ exit(1);
+ }
}
- PQfinish(conn);
termPQExpBuffer(&sql);
}
+/*
+ * Prepare the list of objects to process by querying the catalogs.
+ *
+ * This function will return a SimpleStringList object containing the entire
+ * list of tables in the given database that should be processed by a parallel
+ * database-wide reindex (excluding system tables), or NULL if there's no such
+ * table.
+ */
+static SimpleStringList *
+get_parallel_object_list(PGconn *conn, bool echo)
+{
+ PQExpBufferData catalog_query;
+ PQExpBufferData buf;
+ PGresult *res;
+ SimpleStringList *tables;
+ int ntups,
+ i;
+
+ initPQExpBuffer(&catalog_query);
+
+ /*
+ * This query is run using a safe search_path, so there's no need to fully
+ * qualify everything.
+ */
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE ns.nspname != 'pg_catalog'\n"
+ " AND c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " ORDER BY c.relpages DESC;");
+
+ res = executeQuery(conn, catalog_query.data, echo);
+ termPQExpBuffer(&catalog_query);
+
+ /*
+ * If no rows are returned, there are no matching tables, so we are done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return NULL;
+ }
+
+ tables = pg_malloc0(sizeof(SimpleStringList));
+
+ /* Build qualified identifiers for each table */
+ initPQExpBuffer(&buf);
+ for (i = 0; i < ntups; i++)
+ {
+ appendPQExpBufferStr(&buf,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+
+ simple_string_list_append(tables, buf.data);
+ resetPQExpBuffer(&buf);
+ }
+ termPQExpBuffer(&buf);
+ PQclear(res);
+
+ return tables;
+}
+
static void
reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo, bool quiet, bool verbose,
- bool concurrently)
+ bool concurrently, int concurrentCons)
{
PGconn *conn;
PGresult *result;
@@ -423,9 +620,10 @@ reindex_all_databases(const char *maintenance_db,
appendPQExpBufferStr(&connstr, "dbname=");
appendConnStrVal(&connstr, dbname);
- reindex_one_database(NULL, connstr.data, REINDEX_DATABASE, host,
+ reindex_one_database(connstr.data, REINDEX_DATABASE, NULL, host,
port, username, prompt_password,
- progname, echo, verbose, concurrently);
+ progname, echo, verbose, concurrently,
+ concurrentCons);
}
termPQExpBuffer(&connstr);
@@ -444,6 +642,7 @@ help(const char *progname)
printf(_(" -d, --dbname=DBNAME database to reindex\n"));
printf(_(" -e, --echo show the commands being sent to the server\n"));
printf(_(" -i, --index=INDEX recreate specific index(es) only\n"));
+ printf(_(" -j, --jobs=NUM use this many concurrent connections to reindex\n"));
printf(_(" -q, --quiet don't write any messages\n"));
printf(_(" -s, --system reindex system catalogs\n"));
printf(_(" -S, --schema=SCHEMA reindex specific schema(s) only\n"));
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index 1af8ab70ad..364a771fdc 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -3,7 +3,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 34;
+use Test::More tests => 39;
program_help_ok('reindexdb');
program_version_ok('reindexdb');
@@ -77,3 +77,13 @@ $node->command_ok(
$node->command_ok(
[qw(reindexdb --echo --system dbname=template1)],
'reindexdb system with connection string');
+
+# parallel processing
+$node->command_fails([qw(reindexdb -j2 -s)],
+ 'reindexdb cannot process system catalogs in parallel');
+$node->issues_sql_like([qw(reindexdb -j2)],
+ qr/statement: REINDEX SYSTEM postgres/,
+ 'Global and parallel reindex will issue a REINDEX SYSTEM');
+$node->issues_sql_like([qw(reindexdb -j2)],
+ qr/statement: REINDEX TABLE public.test1/,
+ 'Global and parallel reindex will issue per-table REINDEX');
On Fri, Jul 19, 2019 at 08:29:27AM +0200, Julien Rouhaud wrote:
On Fri, Jul 19, 2019 at 2:35 AM Michael Paquier <michael@paquier.xyz> wrote:
For the second patch, could you send a rebase with a fix for the
connection slot when processing the reindex commands?Attached, I also hopefully removed all the now unneeded progname usage.
+ Note that this mode is not compatible the <option>-i / --index</option>
+ or the <option>-s / --system</option> options.
Nits: this is not a style consistent with the documentation. When
referring to both the long and short options the formulation "-i or
--index" gets used. Here we could just use the long option. This
sentence is missing a "with".
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
The number of items in a simple list is not counted, and vacuumdb does
the same thing to count objects. What do you think about extending
simple lists to track the number of items stored?
+$node->issues_sql_like([qw(reindexdb -j2)],
+ qr/statement: REINDEX TABLE public.test1/,
+ 'Global and parallel reindex will issue per-table REINDEX');
Would it make sense to have some tests for schemas here?
One of my comments in [1]/messages/by-id/20190711040433.GG4500@paquier.xyz -- Michael has not been answered. What about
the decomposition of a list of schemas into a list of tables when
using the parallel mode?
[1]: /messages/by-id/20190711040433.GG4500@paquier.xyz -- Michael
--
Michael
On Mon, Jul 22, 2019 at 6:11 AM Michael Paquier <michael@paquier.xyz> wrote:
On Fri, Jul 19, 2019 at 08:29:27AM +0200, Julien Rouhaud wrote:
On Fri, Jul 19, 2019 at 2:35 AM Michael Paquier <michael@paquier.xyz> wrote:
For the second patch, could you send a rebase with a fix for the
connection slot when processing the reindex commands?Attached, I also hopefully removed all the now unneeded progname usage.
+ Note that this mode is not compatible the <option>-i / --index</option> + or the <option>-s / --system</option> options. Nits: this is not a style consistent with the documentation. When referring to both the long and short options the formulation "-i or --index" gets used. Here we could just use the long option. This sentence is missing a "with".
Right, so I kept the long option. Also this comment was outdated, as
the --jobs is now just ignored with a list of indexes, so I fixed that
too.
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
The number of items in a simple list is not counted, and vacuumdb does
the same thing to count objects. What do you think about extending
simple lists to track the number of items stored?
I considered this, but it would require to adapt all code that declare
SimpleStringList stack variable (vacuumdb.c, clusterdb.c,
createuser.c, pg_dumpall.c and pg_dump.c), so it looked like too much
trouble to avoid 2 local variables here and 1 in vacuumdb.c. I don't
have a strong opinion here, so I can go for it if you prefer.
+$node->issues_sql_like([qw(reindexdb -j2)], + qr/statement: REINDEX TABLE public.test1/, + 'Global and parallel reindex will issue per-table REINDEX'); Would it make sense to have some tests for schemas here?One of my comments in [1] has not been answered. What about
the decomposition of a list of schemas into a list of tables when
using the parallel mode?
I did that in attached v6, and added suitable regression tests.
Attachments:
reindex_parallel_v6.diffapplication/octet-stream; name=reindex_parallel_v6.diffDownload
diff --git a/doc/src/sgml/ref/reindexdb.sgml b/doc/src/sgml/ref/reindexdb.sgml
index 25b5a72770..e5a2a5548a 100644
--- a/doc/src/sgml/ref/reindexdb.sgml
+++ b/doc/src/sgml/ref/reindexdb.sgml
@@ -166,6 +166,30 @@ PostgreSQL documentation
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>-j <replaceable class="parameter">njobs</replaceable></option></term>
+ <term><option>--jobs=<replaceable class="parameter">njobs</replaceable></option></term>
+ <listitem>
+ <para>
+ Execute the reindex commands in parallel by running
+ <replaceable class="parameter">njobs</replaceable>
+ commands simultaneously. This option reduces the time of the
+ processing but it also increases the load on the database server.
+ </para>
+ <para>
+ <application>reindexdb</application> will open
+ <replaceable class="parameter">njobs</replaceable> connections to the
+ database, so make sure your <xref linkend="guc-max-connections"/>
+ setting is high enough to accommodate all connections.
+ </para>
+ <para>
+ Note that this option is ignored with the <option>--index</option>
+ option, and incompatible with the <option>-s / --system</option>
+ option.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>-q</option></term>
<term><option>--quiet</option></term>
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 3cd793b134..ede665090f 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -29,7 +29,7 @@ dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+reindexdb: reindexdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
install: all installdirs
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index 219a9a9211..02d7784e35 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -10,10 +10,14 @@
*/
#include "postgres_fe.h"
+
+#include "catalog/pg_class_d.h"
#include "common.h"
#include "common/logging.h"
+#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
typedef enum ReindexType
{
@@ -25,16 +29,26 @@ typedef enum ReindexType
} ReindexType;
-static void reindex_one_database(const char *name, const char *dbname,
- ReindexType type, const char *host,
+static SimpleStringList *get_parallel_object_list(PGconn *conn,
+ ReindexType type,
+ SimpleStringList *user_list,
+ bool echo);
+static void reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
const char *port, const char *username,
enum trivalue prompt_password, const char *progname,
- bool echo, bool verbose, bool concurrently);
+ bool echo, bool verbose, bool concurrently,
+ int concurrentCons);
static void reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo,
- bool quiet, bool verbose, bool concurrently);
+ bool quiet, bool verbose, bool concurrently,
+ int concurrentCons);
+static void run_reindex_command(PGconn *conn, ReindexType type,
+ const char *name, bool echo, bool verbose,
+ bool concurrently, bool async);
+
static void help(const char *progname);
int
@@ -54,6 +68,7 @@ main(int argc, char *argv[])
{"system", no_argument, NULL, 's'},
{"table", required_argument, NULL, 't'},
{"index", required_argument, NULL, 'i'},
+ {"jobs", required_argument, NULL, 'j'},
{"verbose", no_argument, NULL, 'v'},
{"concurrently", no_argument, NULL, 1},
{"maintenance-db", required_argument, NULL, 2},
@@ -79,6 +94,9 @@ main(int argc, char *argv[])
SimpleStringList indexes = {NULL, NULL};
SimpleStringList tables = {NULL, NULL};
SimpleStringList schemas = {NULL, NULL};
+ int concurrentCons = 1;
+ int tbl_count = 0,
+ nsp_count = 0;
pg_logging_init(argv[0]);
progname = get_progname(argv[0]);
@@ -87,7 +105,7 @@ main(int argc, char *argv[])
handle_help_version_opts(argc, argv, "reindexdb", help);
/* process command-line options */
- while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:v", long_options, &optindex)) != -1)
+ while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:j:v", long_options, &optindex)) != -1)
{
switch (c)
{
@@ -114,6 +132,7 @@ main(int argc, char *argv[])
break;
case 'S':
simple_string_list_append(&schemas, optarg);
+ nsp_count++;
break;
case 'd':
dbname = pg_strdup(optarg);
@@ -126,10 +145,25 @@ main(int argc, char *argv[])
break;
case 't':
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
case 'i':
simple_string_list_append(&indexes, optarg);
break;
+ case 'j':
+ concurrentCons = atoi(optarg);
+ if (concurrentCons <= 0)
+ {
+ pg_log_error("number of parallel jobs must be at least 1");
+ exit(1);
+ }
+ if (concurrentCons > FD_SETSIZE - 1)
+ {
+ pg_log_error("too many parallel jobs requested (maximum: %d)",
+ FD_SETSIZE - 1);
+ exit(1);
+ }
+ break;
case 'v':
verbose = true;
break;
@@ -194,7 +228,8 @@ main(int argc, char *argv[])
}
reindex_all_databases(maintenance_db, host, port, username,
- prompt_password, progname, echo, quiet, verbose, concurrently);
+ prompt_password, progname, echo, quiet, verbose,
+ concurrently, concurrentCons);
}
else if (syscatalog)
{
@@ -214,6 +249,12 @@ main(int argc, char *argv[])
exit(1);
}
+ if (concurrentCons > 1)
+ {
+ pg_log_error("cannot use multiple jobs to reindex system catalogs");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -224,9 +265,9 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- reindex_one_database(NULL, dbname, REINDEX_SYSTEM, host,
+ reindex_one_database(dbname, REINDEX_SYSTEM, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, 1);
}
else
{
@@ -241,61 +282,55 @@ main(int argc, char *argv[])
}
if (schemas.head != NULL)
- {
- SimpleStringListCell *cell;
-
- for (cell = schemas.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_SCHEMA, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, nsp_count));
if (indexes.head != NULL)
- {
- SimpleStringListCell *cell;
- for (cell = indexes.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_INDEX, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
- if (tables.head != NULL)
- {
- SimpleStringListCell *cell;
+ /*
+ * An index list cannot be processed by multiple connections, as
+ * it would require to distribute the work by owner tables. We
+ * simply ignore the passed number of jobs if any.
+ */
+ reindex_one_database(dbname, REINDEX_INDEX, &indexes, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently, 1);
- for (cell = tables.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_TABLE, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ if (tables.head != NULL)
+ reindex_one_database(dbname, REINDEX_TABLE, &tables, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, tbl_count));
/*
* reindex database only if neither index nor table nor schema is
* specified
*/
if (indexes.head == NULL && tables.head == NULL && schemas.head == NULL)
- reindex_one_database(NULL, dbname, REINDEX_DATABASE, host,
+ reindex_one_database(dbname, REINDEX_DATABASE, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, concurrentCons);
}
exit(0);
}
static void
-reindex_one_database(const char *name, const char *dbname, ReindexType type,
- const char *host, const char *port, const char *username,
+reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
+ const char *port, const char *username,
enum trivalue prompt_password, const char *progname, bool echo,
- bool verbose, bool concurrently)
+ bool verbose, bool concurrently, int concurrentCons)
{
- PQExpBufferData sql;
PGconn *conn;
+ SimpleStringListCell *cell;
+ bool parallel = concurrentCons > 1;
+ SimpleStringList *process_list = user_list;
+ ReindexType process_type = type;
+ ParallelSlot *slots;
+ bool failed = false;
conn = connectDatabase(dbname, host, port, username, prompt_password,
progname, echo, false, false);
@@ -308,6 +343,107 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
exit(1);
}
+ if (!parallel)
+ {
+ if (user_list == NULL)
+ {
+ /*
+ * Create a dummy list with an empty string, as user requires an
+ * element.
+ */
+ process_list = pg_malloc0(sizeof(SimpleStringList));
+ simple_string_list_append(process_list, "");
+ }
+ }
+ else
+ {
+ /*
+ * Database-wide parallel reindex requires special processing. If
+ * multiple jobs were asked, we have to reindex system catalogs first,
+ * as they can't be processed in parallel.
+ */
+ if (process_type == REINDEX_DATABASE)
+ {
+ Assert(user_list == NULL);
+ run_reindex_command(conn, REINDEX_SYSTEM, NULL, echo, verbose,
+ concurrently, false);
+
+ process_list = get_parallel_object_list(conn, process_type,
+ user_list, echo);
+ process_type = REINDEX_TABLE;
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ }
+ else if (process_type == REINDEX_SCHEMA)
+ {
+ Assert(user_list != NULL);
+
+ process_list = get_parallel_object_list(conn, process_type,
+ user_list, echo);
+ process_type = REINDEX_TABLE;
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ }
+ else
+ Assert(user_list != NULL);
+ }
+
+ slots = ParallelSlotsSetup(dbname, host, port, username, prompt_password,
+ progname, echo, conn, concurrentCons);
+
+ cell = process_list->head;
+ do
+ {
+ const char *objname = cell->val;
+ ParallelSlot *free_slot = NULL;
+
+ if (CancelRequested)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ free_slot = ParallelSlotsGetIdle(slots, concurrentCons);
+ if (!free_slot)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ run_reindex_command(free_slot->connection, process_type, objname,
+ echo, verbose, concurrently, true);
+
+ cell = cell->next;
+ } while (cell != NULL);
+
+ if (!ParallelSlotsWaitCompletion(slots, concurrentCons))
+ failed = true;
+
+finish:
+ ParallelSlotsTerminate(slots, concurrentCons);
+ pfree(slots);
+
+ if (failed)
+ exit(1);
+}
+
+static void
+run_reindex_command(PGconn *conn, ReindexType type, const char *name,
+ bool echo, bool verbose, bool concurrently, bool async)
+{
+ PQExpBufferData sql;
+ bool status;
+
/* build the REINDEX query */
initPQExpBuffer(&sql);
@@ -358,7 +494,17 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
/* finish the query */
appendPQExpBufferChar(&sql, ';');
- if (!executeMaintenanceCommand(conn, sql.data, echo))
+ if (async)
+ {
+ if (echo)
+ printf("%s\n", sql.data);
+
+ status = PQsendQuery(conn, sql.data) == 1;
+ }
+ else
+ status = executeMaintenanceCommand(conn, sql.data, echo);
+
+ if (!status)
{
switch (type)
{
@@ -383,20 +529,131 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
name, PQdb(conn), PQerrorMessage(conn));
break;
}
- PQfinish(conn);
- exit(1);
+ if (!async)
+ {
+ PQfinish(conn);
+ exit(1);
+ }
}
- PQfinish(conn);
termPQExpBuffer(&sql);
}
+/*
+ * Prepare the list of objects to process by querying the catalogs.
+ *
+ * This function will return a SimpleStringList object containing the entire
+ * list of tables in the given database that should be processed by a parallel
+ * database-wide reindex (excluding system tables), or NULL if there's no such
+ * table.
+ */
+static SimpleStringList *
+get_parallel_object_list(PGconn *conn, ReindexType type,
+ SimpleStringList *user_list, bool echo)
+{
+ PQExpBufferData catalog_query;
+ PQExpBufferData buf;
+ PGresult *res;
+ SimpleStringList *tables;
+ int ntups,
+ i;
+
+ initPQExpBuffer(&catalog_query);
+
+ /*
+ * The queries here are using a safe search_path, so there's no need to
+ * fully qualify everything.
+ */
+ if (type == REINDEX_DATABASE)
+ {
+ Assert(user_list == NULL);
+
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE ns.nspname != 'pg_catalog'\n"
+ " AND c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " ORDER BY c.relpages DESC;");
+ }
+ else if (type == REINDEX_SCHEMA)
+ {
+ SimpleStringListCell *cell;
+ bool nsp_listed = false;
+
+ Assert(user_list != NULL);
+
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " AND ns.nspname IN (");
+
+ for (cell = user_list->head; cell; cell = cell->next)
+ {
+ const char *nspname = cell->val;
+
+ if (nsp_listed)
+ appendPQExpBuffer(&catalog_query, ", ");
+ else
+ nsp_listed = true;
+
+ appendStringLiteralConn(&catalog_query, nspname, conn);
+
+ }
+
+ appendPQExpBuffer(&catalog_query, ")\n"
+ " ORDER BY c.relpages DESC;");
+ }
+ else
+ Assert(false);
+
+ res = executeQuery(conn, catalog_query.data, echo);
+ termPQExpBuffer(&catalog_query);
+
+ /*
+ * If no rows are returned, there are no matching tables, so we are done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return NULL;
+ }
+
+ tables = pg_malloc0(sizeof(SimpleStringList));
+
+ /* Build qualified identifiers for each table */
+ initPQExpBuffer(&buf);
+ for (i = 0; i < ntups; i++)
+ {
+ appendPQExpBufferStr(&buf,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+
+ simple_string_list_append(tables, buf.data);
+ resetPQExpBuffer(&buf);
+ }
+ termPQExpBuffer(&buf);
+ PQclear(res);
+
+ return tables;
+}
+
static void
reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo, bool quiet, bool verbose,
- bool concurrently)
+ bool concurrently, int concurrentCons)
{
PGconn *conn;
PGresult *result;
@@ -423,9 +680,10 @@ reindex_all_databases(const char *maintenance_db,
appendPQExpBufferStr(&connstr, "dbname=");
appendConnStrVal(&connstr, dbname);
- reindex_one_database(NULL, connstr.data, REINDEX_DATABASE, host,
+ reindex_one_database(connstr.data, REINDEX_DATABASE, NULL, host,
port, username, prompt_password,
- progname, echo, verbose, concurrently);
+ progname, echo, verbose, concurrently,
+ concurrentCons);
}
termPQExpBuffer(&connstr);
@@ -444,6 +702,7 @@ help(const char *progname)
printf(_(" -d, --dbname=DBNAME database to reindex\n"));
printf(_(" -e, --echo show the commands being sent to the server\n"));
printf(_(" -i, --index=INDEX recreate specific index(es) only\n"));
+ printf(_(" -j, --jobs=NUM use this many concurrent connections to reindex\n"));
printf(_(" -q, --quiet don't write any messages\n"));
printf(_(" -s, --system reindex system catalogs\n"));
printf(_(" -S, --schema=SCHEMA reindex specific schema(s) only\n"));
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index 1af8ab70ad..f2a4fc9bba 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -3,7 +3,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 34;
+use Test::More tests => 41;
program_help_ok('reindexdb');
program_version_ok('reindexdb');
@@ -77,3 +77,25 @@ $node->command_ok(
$node->command_ok(
[qw(reindexdb --echo --system dbname=template1)],
'reindexdb system with connection string');
+
+# parallel processing
+$node->safe_psql('postgres', q|
+ CREATE SCHEMA s1;
+ CREATE TABLE s1.t1(id integer);
+ CREATE INDEX ON s1.t1(id);
+ CREATE SCHEMA s2;
+ CREATE TABLE s2.t2(id integer);
+ CREATE INDEX ON s2.t2(id);
+|);
+
+$node->command_fails([qw(reindexdb -j2 -s)],
+ 'reindexdb cannot process system catalogs in parallel');
+$node->issues_sql_like([qw(reindexdb -j2)],
+ qr/statement: REINDEX SYSTEM postgres/,
+ 'Global and parallel reindex will issue a REINDEX SYSTEM');
+$node->issues_sql_like([qw(reindexdb -j2)],
+ qr/statement: REINDEX TABLE public.test1/,
+ 'Global and parallel reindex will issue per-table REINDEX');
+$node->issues_sql_like([qw(reindexdb -j2 -S s1 -S s2)],
+ qr/statement: REINDEX TABLE s1.t1/,
+ 'Schema-specific and parallel reindex will issue per-table REINDEX');
On 2019-Jul-22, Julien Rouhaud wrote:
On Mon, Jul 22, 2019 at 6:11 AM Michael Paquier <michael@paquier.xyz> wrote:
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
The number of items in a simple list is not counted, and vacuumdb does
the same thing to count objects. What do you think about extending
simple lists to track the number of items stored?I considered this, but it would require to adapt all code that declare
SimpleStringList stack variable (vacuumdb.c, clusterdb.c,
createuser.c, pg_dumpall.c and pg_dump.c), so it looked like too much
trouble to avoid 2 local variables here and 1 in vacuumdb.c. I don't
have a strong opinion here, so I can go for it if you prefer.
Can we use List for this instead?
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On 2019-Jul-19, Julien Rouhaud wrote:
For the second patch, could you send a rebase with a fix for the
connection slot when processing the reindex commands?Attached, I also hopefully removed all the now unneeded progname usage.
BTW "progname" is a global variable in logging.c, and it's initialized
by pg_logging_init(), so there's no point in having a local variable in
main() that's called the same and initialized the same way. You could
just remove it from the signature of all those functions
(connectDatabase and callers), and there would be no visible change.
Also: [see attached]
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Attachments:
less-progname.patchtext/x-diff; charset=us-asciiDownload
commit 957e56dee9a8b32f8f409a516a0195ceb3bc6a75
Author: Alvaro Herrera <alvherre@alvh.no-ip.org>
AuthorDate: Mon Jul 22 11:09:18 2019 -0400
CommitDate: Mon Jul 22 11:09:30 2019 -0400
remove useless progname
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index d3ee0da917..d81bfa3a6b 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -57,7 +57,7 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
vacuumingOptions *vacopts, const char *table);
static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
- const char *table, const char *progname);
+ const char *table);
static void help(const char *progname);
@@ -646,7 +646,7 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
* through ParallelSlotsGetIdle.
*/
run_vacuum_command(free_slot->connection, sql.data,
- echo, tabname, progname);
+ echo, tabname);
cell = cell->next;
} while (cell != NULL);
@@ -855,7 +855,7 @@ prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
*/
static void
run_vacuum_command(PGconn *conn, const char *sql, bool echo,
- const char *table, const char *progname)
+ const char *table)
{
bool status;
On Mon, Jul 22, 2019 at 5:11 PM Alvaro Herrera <alvherre@2ndquadrant.com> wrote:
On 2019-Jul-22, Julien Rouhaud wrote:
On Mon, Jul 22, 2019 at 6:11 AM Michael Paquier <michael@paquier.xyz> wrote:
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
The number of items in a simple list is not counted, and vacuumdb does
the same thing to count objects. What do you think about extending
simple lists to track the number of items stored?I considered this, but it would require to adapt all code that declare
SimpleStringList stack variable (vacuumdb.c, clusterdb.c,
createuser.c, pg_dumpall.c and pg_dump.c), so it looked like too much
trouble to avoid 2 local variables here and 1 in vacuumdb.c. I don't
have a strong opinion here, so I can go for it if you prefer.Can we use List for this instead?
Isn't that for backend code only?
On 2019-Jul-22, Julien Rouhaud wrote:
On Mon, Jul 22, 2019 at 5:11 PM Alvaro Herrera <alvherre@2ndquadrant.com> wrote:
I considered this, but it would require to adapt all code that declare
SimpleStringList stack variable (vacuumdb.c, clusterdb.c,
createuser.c, pg_dumpall.c and pg_dump.c), so it looked like too much
trouble to avoid 2 local variables here and 1 in vacuumdb.c. I don't
have a strong opinion here, so I can go for it if you prefer.Can we use List for this instead?
Isn't that for backend code only?
Well, we already have palloc() on the frontend side, and list.c doesn't
have any elog()/ereport(), so it should be possible to use it ... I do
see that it uses MemoryContextAlloc() in a few places. Maybe we can
just #define that to palloc()?
(Maybe we can use the impulse to get rid of these "simple lists"
altogether?)
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Alvaro Herrera <alvherre@2ndquadrant.com> writes:
On 2019-Jul-22, Julien Rouhaud wrote:
On Mon, Jul 22, 2019 at 5:11 PM Alvaro Herrera <alvherre@2ndquadrant.com> wrote:
Can we use List for this instead?
Isn't that for backend code only?
Well, we already have palloc() on the frontend side, and list.c doesn't
have any elog()/ereport(), so it should be possible to use it ... I do
see that it uses MemoryContextAlloc() in a few places. Maybe we can
just #define that to palloc()?
I'm not happy about either the idea of pulling all of list.c into
frontend programs, or restricting it to be frontend-safe. That's
very fundamental infrastructure and I don't want it laboring under
such a restriction. Furthermore, List usage generally leaks memory
like mad (cf nearby list_concat discussion) which doesn't seem like
something we want for frontend code.
regards, tom lane
On 2019-Jul-22, Tom Lane wrote:
Alvaro Herrera <alvherre@2ndquadrant.com> writes:
On 2019-Jul-22, Julien Rouhaud wrote:
On Mon, Jul 22, 2019 at 5:11 PM Alvaro Herrera <alvherre@2ndquadrant.com> wrote:
Can we use List for this instead?
Isn't that for backend code only?
Well, we already have palloc() on the frontend side, and list.c doesn't
have any elog()/ereport(), so it should be possible to use it ... I do
see that it uses MemoryContextAlloc() in a few places. Maybe we can
just #define that to palloc()?I'm not happy about either the idea of pulling all of list.c into
frontend programs, or restricting it to be frontend-safe. That's
very fundamental infrastructure and I don't want it laboring under
such a restriction. Furthermore, List usage generally leaks memory
like mad (cf nearby list_concat discussion) which doesn't seem like
something we want for frontend code.
Fair enough. List has gotten quite sophisticated now, so I understand
the concern.
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Mon, Jul 22, 2019 at 11:18:06AM -0400, Alvaro Herrera wrote:
BTW "progname" is a global variable in logging.c, and it's initialized
by pg_logging_init(), so there's no point in having a local variable in
main() that's called the same and initialized the same way. You could
just remove it from the signature of all those functions
(connectDatabase and callers), and there would be no visible change.
Sure, and I was really tempted to do that until I noticed that we pass
down progname for fallback_application_name in the connection string
and that we would basically need to externalize progname in logging.h,
as well as switch all the callers of pg_logging_init to now include
their own definition of progname, which was much more invasive than
the initial refactoring intended. I am also under the impression that
we had better keep get_progname() and pg_logging_init() as rather
independent things.
Also: [see attached]
Missed those in the initial cleanup. Applied, thanks!
--
Michael
On Mon, Jul 22, 2019 at 02:40:19PM +0200, Julien Rouhaud wrote:
Right, so I kept the long option. Also this comment was outdated, as
the --jobs is now just ignored with a list of indexes, so I fixed that
too.
+ if (!parallel)
+ {
+ if (user_list == NULL)
+ {
+ /*
+ * Create a dummy list with an empty string, as user requires an
+ * element.
+ */
+ process_list = pg_malloc0(sizeof(SimpleStringList));
+ simple_string_list_append(process_list, "");
+ }
+ }
This part looks a bit hacky and this is needed because we don't have a
list of objects when doing a non-parallel system or database reindex.
The deal is that we just want a list with one element: the database of
the connection. Wouldn't it be more natural to assign the database
name here using PQdb(conn)? Then add an assertion at the top of
run_reindex_command() checking for a non-NULL name?
I considered this, but it would require to adapt all code that declare
SimpleStringList stack variable (vacuumdb.c, clusterdb.c,
createuser.c, pg_dumpall.c and pg_dump.c), so it looked like too much
trouble to avoid 2 local variables here and 1 in vacuumdb.c. I don't
have a strong opinion here, so I can go for it if you prefer.
Okay. This is a tad wider than the original patch proposal, and this
adds two lines. So let's discard that for now and keep it simple.
+$node->issues_sql_like([qw(reindexdb -j2)], + qr/statement: REINDEX TABLE public.test1/, + 'Global and parallel reindex will issue per-table REINDEX'); Would it make sense to have some tests for schemas here?One of my comments in [1] has not been answered. What about
the decomposition of a list of schemas into a list of tables when
using the parallel mode?I did that in attached v6, and added suitable regression tests.
The two tests for "reindexdb -j2" can be combined into a single call,
checking for both commands to be generated in the same output. The
second command triggering a reindex on two schemas cannot be used to
check for the generation of both s1.t1 and s2.t2 as the ordering may
not be guaranteed. The commands arrays also looked inconsistent with
the rest. Attached is an updated patch with some format modifications
and the fixes for the tests.
--
Michael
Attachments:
reindex_parallel_v7.patchtext/x-diff; charset=us-asciiDownload
diff --git a/doc/src/sgml/ref/reindexdb.sgml b/doc/src/sgml/ref/reindexdb.sgml
index 25b5a72770..477b77c6e9 100644
--- a/doc/src/sgml/ref/reindexdb.sgml
+++ b/doc/src/sgml/ref/reindexdb.sgml
@@ -166,6 +166,31 @@ PostgreSQL documentation
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>-j <replaceable class="parameter">njobs</replaceable></option></term>
+ <term><option>--jobs=<replaceable class="parameter">njobs</replaceable></option></term>
+ <listitem>
+ <para>
+ Execute the reindex commands in parallel by running
+ <replaceable class="parameter">njobs</replaceable>
+ commands simultaneously. This option reduces the time of the
+ processing but it also increases the load on the database server.
+ </para>
+ <para>
+ <application>reindexdb</application> will open
+ <replaceable class="parameter">njobs</replaceable> connections to the
+ database, so make sure your <xref linkend="guc-max-connections"/>
+ setting is high enough to accommodate all connections.
+ </para>
+ <para>
+ Note that this option is ignored with the <option>--index</option>
+ option to prevent deadlocks when processing multiple indexes from
+ the same relation, and incompatible with the <option>--system</option>
+ option.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>-q</option></term>
<term><option>--quiet</option></term>
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 3cd793b134..ede665090f 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -29,7 +29,7 @@ dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+reindexdb: reindexdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
install: all installdirs
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index 219a9a9211..895dc03737 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -10,10 +10,14 @@
*/
#include "postgres_fe.h"
+
+#include "catalog/pg_class_d.h"
#include "common.h"
#include "common/logging.h"
+#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
typedef enum ReindexType
{
@@ -25,16 +29,26 @@ typedef enum ReindexType
} ReindexType;
-static void reindex_one_database(const char *name, const char *dbname,
- ReindexType type, const char *host,
+static SimpleStringList *get_parallel_object_list(PGconn *conn,
+ ReindexType type,
+ SimpleStringList *user_list,
+ bool echo);
+static void reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
const char *port, const char *username,
enum trivalue prompt_password, const char *progname,
- bool echo, bool verbose, bool concurrently);
+ bool echo, bool verbose, bool concurrently,
+ int concurrentCons);
static void reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo,
- bool quiet, bool verbose, bool concurrently);
+ bool quiet, bool verbose, bool concurrently,
+ int concurrentCons);
+static void run_reindex_command(PGconn *conn, ReindexType type,
+ const char *name, bool echo, bool verbose,
+ bool concurrently, bool async);
+
static void help(const char *progname);
int
@@ -54,6 +68,7 @@ main(int argc, char *argv[])
{"system", no_argument, NULL, 's'},
{"table", required_argument, NULL, 't'},
{"index", required_argument, NULL, 'i'},
+ {"jobs", required_argument, NULL, 'j'},
{"verbose", no_argument, NULL, 'v'},
{"concurrently", no_argument, NULL, 1},
{"maintenance-db", required_argument, NULL, 2},
@@ -79,6 +94,9 @@ main(int argc, char *argv[])
SimpleStringList indexes = {NULL, NULL};
SimpleStringList tables = {NULL, NULL};
SimpleStringList schemas = {NULL, NULL};
+ int concurrentCons = 1;
+ int tbl_count = 0,
+ nsp_count = 0;
pg_logging_init(argv[0]);
progname = get_progname(argv[0]);
@@ -87,7 +105,7 @@ main(int argc, char *argv[])
handle_help_version_opts(argc, argv, "reindexdb", help);
/* process command-line options */
- while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:v", long_options, &optindex)) != -1)
+ while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:j:v", long_options, &optindex)) != -1)
{
switch (c)
{
@@ -114,6 +132,7 @@ main(int argc, char *argv[])
break;
case 'S':
simple_string_list_append(&schemas, optarg);
+ nsp_count++;
break;
case 'd':
dbname = pg_strdup(optarg);
@@ -126,10 +145,25 @@ main(int argc, char *argv[])
break;
case 't':
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
case 'i':
simple_string_list_append(&indexes, optarg);
break;
+ case 'j':
+ concurrentCons = atoi(optarg);
+ if (concurrentCons <= 0)
+ {
+ pg_log_error("number of parallel jobs must be at least 1");
+ exit(1);
+ }
+ if (concurrentCons > FD_SETSIZE - 1)
+ {
+ pg_log_error("too many parallel jobs requested (maximum: %d)",
+ FD_SETSIZE - 1);
+ exit(1);
+ }
+ break;
case 'v':
verbose = true;
break;
@@ -194,7 +228,8 @@ main(int argc, char *argv[])
}
reindex_all_databases(maintenance_db, host, port, username,
- prompt_password, progname, echo, quiet, verbose, concurrently);
+ prompt_password, progname, echo, quiet, verbose,
+ concurrently, concurrentCons);
}
else if (syscatalog)
{
@@ -214,6 +249,12 @@ main(int argc, char *argv[])
exit(1);
}
+ if (concurrentCons > 1)
+ {
+ pg_log_error("cannot use multiple jobs to reindex system catalogs");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -224,9 +265,9 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- reindex_one_database(NULL, dbname, REINDEX_SYSTEM, host,
+ reindex_one_database(dbname, REINDEX_SYSTEM, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, 1);
}
else
{
@@ -241,61 +282,57 @@ main(int argc, char *argv[])
}
if (schemas.head != NULL)
- {
- SimpleStringListCell *cell;
-
- for (cell = schemas.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_SCHEMA, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, nsp_count));
if (indexes.head != NULL)
{
- SimpleStringListCell *cell;
-
- for (cell = indexes.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_INDEX, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
+ /*
+ * An index list cannot be processed by multiple connections, as
+ * it could cause conflicts if reindexing multiple indexes from
+ * the same table. We simply ignore the passed number of jobs if
+ * any.
+ */
+ reindex_one_database(dbname, REINDEX_INDEX, &indexes, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently, 1);
}
+
if (tables.head != NULL)
- {
- SimpleStringListCell *cell;
-
- for (cell = tables.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_TABLE, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ reindex_one_database(dbname, REINDEX_TABLE, &tables, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, tbl_count));
/*
* reindex database only if neither index nor table nor schema is
* specified
*/
if (indexes.head == NULL && tables.head == NULL && schemas.head == NULL)
- reindex_one_database(NULL, dbname, REINDEX_DATABASE, host,
+ reindex_one_database(dbname, REINDEX_DATABASE, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, concurrentCons);
}
exit(0);
}
static void
-reindex_one_database(const char *name, const char *dbname, ReindexType type,
- const char *host, const char *port, const char *username,
+reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
+ const char *port, const char *username,
enum trivalue prompt_password, const char *progname, bool echo,
- bool verbose, bool concurrently)
+ bool verbose, bool concurrently, int concurrentCons)
{
- PQExpBufferData sql;
PGconn *conn;
+ SimpleStringListCell *cell;
+ bool parallel = concurrentCons > 1;
+ SimpleStringList *process_list = user_list;
+ ReindexType process_type = type;
+ ParallelSlot *slots;
+ bool failed = false;
conn = connectDatabase(dbname, host, port, username, prompt_password,
progname, echo, false, false);
@@ -308,6 +345,107 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
exit(1);
}
+ if (!parallel)
+ {
+ if (user_list == NULL)
+ {
+ /*
+ * Create a dummy list with an empty string, as user requires an
+ * element.
+ */
+ process_list = pg_malloc0(sizeof(SimpleStringList));
+ simple_string_list_append(process_list, "");
+ }
+ }
+ else
+ {
+ /*
+ * Database-wide parallel reindex requires special processing. If
+ * multiple jobs were asked, we have to reindex system catalogs first,
+ * as they can't be processed in parallel.
+ */
+ if (process_type == REINDEX_DATABASE)
+ {
+ Assert(user_list == NULL);
+ run_reindex_command(conn, REINDEX_SYSTEM, NULL, echo, verbose,
+ concurrently, false);
+
+ process_list = get_parallel_object_list(conn, process_type,
+ user_list, echo);
+ process_type = REINDEX_TABLE;
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ }
+ else if (process_type == REINDEX_SCHEMA)
+ {
+ Assert(user_list != NULL);
+
+ process_list = get_parallel_object_list(conn, process_type,
+ user_list, echo);
+ process_type = REINDEX_TABLE;
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ }
+ else
+ Assert(user_list != NULL);
+ }
+
+ slots = ParallelSlotsSetup(dbname, host, port, username, prompt_password,
+ progname, echo, conn, concurrentCons);
+
+ cell = process_list->head;
+ do
+ {
+ const char *objname = cell->val;
+ ParallelSlot *free_slot = NULL;
+
+ if (CancelRequested)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ free_slot = ParallelSlotsGetIdle(slots, concurrentCons);
+ if (!free_slot)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ run_reindex_command(free_slot->connection, process_type, objname,
+ echo, verbose, concurrently, true);
+
+ cell = cell->next;
+ } while (cell != NULL);
+
+ if (!ParallelSlotsWaitCompletion(slots, concurrentCons))
+ failed = true;
+
+finish:
+ ParallelSlotsTerminate(slots, concurrentCons);
+ pfree(slots);
+
+ if (failed)
+ exit(1);
+}
+
+static void
+run_reindex_command(PGconn *conn, ReindexType type, const char *name,
+ bool echo, bool verbose, bool concurrently, bool async)
+{
+ PQExpBufferData sql;
+ bool status;
+
/* build the REINDEX query */
initPQExpBuffer(&sql);
@@ -358,7 +496,17 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
/* finish the query */
appendPQExpBufferChar(&sql, ';');
- if (!executeMaintenanceCommand(conn, sql.data, echo))
+ if (async)
+ {
+ if (echo)
+ printf("%s\n", sql.data);
+
+ status = PQsendQuery(conn, sql.data) == 1;
+ }
+ else
+ status = executeMaintenanceCommand(conn, sql.data, echo);
+
+ if (!status)
{
switch (type)
{
@@ -383,20 +531,135 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
name, PQdb(conn), PQerrorMessage(conn));
break;
}
- PQfinish(conn);
- exit(1);
+ if (!async)
+ {
+ PQfinish(conn);
+ exit(1);
+ }
}
- PQfinish(conn);
termPQExpBuffer(&sql);
}
+/*
+ * Prepare the list of objects to process by querying the catalogs.
+ *
+ * This function will return a SimpleStringList object containing the entire
+ * list of tables in the given database that should be processed by a parallel
+ * database-wide reindex (excluding system tables), or NULL if there's no such
+ * table.
+ */
+static SimpleStringList *
+get_parallel_object_list(PGconn *conn, ReindexType type,
+ SimpleStringList *user_list, bool echo)
+{
+ PQExpBufferData catalog_query;
+ PQExpBufferData buf;
+ PGresult *res;
+ SimpleStringList *tables;
+ int ntups,
+ i;
+
+ initPQExpBuffer(&catalog_query);
+
+ /*
+ * The queries here are using a safe search_path, so there's no need to
+ * fully qualify everything.
+ */
+ if (type == REINDEX_DATABASE)
+ {
+ Assert(user_list == NULL);
+
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE ns.nspname != 'pg_catalog'\n"
+ " AND c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " ORDER BY c.relpages DESC;");
+ }
+ else if (type == REINDEX_SCHEMA)
+ {
+ SimpleStringListCell *cell;
+ bool nsp_listed = false;
+
+ Assert(user_list != NULL);
+
+ /*
+ * All the tables from all the listed schemas are grabbed at
+ * once.
+ */
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " AND ns.nspname IN (");
+
+ for (cell = user_list->head; cell; cell = cell->next)
+ {
+ const char *nspname = cell->val;
+
+ if (nsp_listed)
+ appendPQExpBuffer(&catalog_query, ", ");
+ else
+ nsp_listed = true;
+
+ appendStringLiteralConn(&catalog_query, nspname, conn);
+
+ }
+
+ appendPQExpBuffer(&catalog_query, ")\n"
+ " ORDER BY c.relpages DESC;");
+ }
+ else
+ Assert(false);
+
+ res = executeQuery(conn, catalog_query.data, echo);
+ termPQExpBuffer(&catalog_query);
+
+ /*
+ * If no rows are returned, there are no matching tables, so we are done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return NULL;
+ }
+
+ tables = pg_malloc0(sizeof(SimpleStringList));
+
+ /* Build qualified identifiers for each table */
+ initPQExpBuffer(&buf);
+ for (i = 0; i < ntups; i++)
+ {
+ appendPQExpBufferStr(&buf,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+
+ simple_string_list_append(tables, buf.data);
+ resetPQExpBuffer(&buf);
+ }
+ termPQExpBuffer(&buf);
+ PQclear(res);
+
+ return tables;
+}
+
static void
reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo, bool quiet, bool verbose,
- bool concurrently)
+ bool concurrently, int concurrentCons)
{
PGconn *conn;
PGresult *result;
@@ -423,9 +686,10 @@ reindex_all_databases(const char *maintenance_db,
appendPQExpBufferStr(&connstr, "dbname=");
appendConnStrVal(&connstr, dbname);
- reindex_one_database(NULL, connstr.data, REINDEX_DATABASE, host,
+ reindex_one_database(connstr.data, REINDEX_DATABASE, NULL, host,
port, username, prompt_password,
- progname, echo, verbose, concurrently);
+ progname, echo, verbose, concurrently,
+ concurrentCons);
}
termPQExpBuffer(&connstr);
@@ -444,6 +708,7 @@ help(const char *progname)
printf(_(" -d, --dbname=DBNAME database to reindex\n"));
printf(_(" -e, --echo show the commands being sent to the server\n"));
printf(_(" -i, --index=INDEX recreate specific index(es) only\n"));
+ printf(_(" -j, --jobs=NUM use this many concurrent connections to reindex\n"));
printf(_(" -q, --quiet don't write any messages\n"));
printf(_(" -s, --system reindex system catalogs\n"));
printf(_(" -S, --schema=SCHEMA reindex specific schema(s) only\n"));
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index 1af8ab70ad..d058690892 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -3,7 +3,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 34;
+use Test::More tests => 39;
program_help_ok('reindexdb');
program_version_ok('reindexdb');
@@ -77,3 +77,27 @@ $node->command_ok(
$node->command_ok(
[qw(reindexdb --echo --system dbname=template1)],
'reindexdb system with connection string');
+
+# parallel processing
+$node->safe_psql(
+ 'postgres', q|
+ CREATE SCHEMA s1;
+ CREATE TABLE s1.t1(id integer);
+ CREATE INDEX ON s1.t1(id);
+ CREATE SCHEMA s2;
+ CREATE TABLE s2.t2(id integer);
+ CREATE INDEX ON s2.t2(id);
+|);
+
+$node->command_fails(
+ [ 'reindexdb', '-j', '2', '-s', 'postgres' ],
+ 'reindexdb cannot process system catalogs in parallel');
+$node->issues_sql_like(
+ [ 'reindexdb', '-j', '2', 'postgres' ],
+ qr/statement:\ REINDEX SYSTEM postgres;
+.*statement:\ REINDEX TABLE public\.test1/s,
+ 'global and parallel reindex issues REINDEX SYSTEM');
+$node->issues_sql_like(
+ [ 'reindexdb', '-j', '2', '-S', 's1', '-S', 's2', 'postgres' ],
+ qr/statement:\ REINDEX TABLE s1.t1;/,
+ 'Schema-specific and parallel reindex will issue per-table REINDEX');
Sorry for the late answer,
On Tue, Jul 23, 2019 at 9:38 AM Michael Paquier <michael@paquier.xyz> wrote:
On Mon, Jul 22, 2019 at 02:40:19PM +0200, Julien Rouhaud wrote:
Right, so I kept the long option. Also this comment was outdated, as
the --jobs is now just ignored with a list of indexes, so I fixed that
too.+ if (!parallel) + { + if (user_list == NULL) + { + /* + * Create a dummy list with an empty string, as user requires an + * element. + */ + process_list = pg_malloc0(sizeof(SimpleStringList)); + simple_string_list_append(process_list, ""); + } + } This part looks a bit hacky and this is needed because we don't have a list of objects when doing a non-parallel system or database reindex. The deal is that we just want a list with one element: the database of the connection. Wouldn't it be more natural to assign the database name here using PQdb(conn)? Then add an assertion at the top of run_reindex_command() checking for a non-NULL name?
Good point, fixed it that way.
I considered this, but it would require to adapt all code that declare
SimpleStringList stack variable (vacuumdb.c, clusterdb.c,
createuser.c, pg_dumpall.c and pg_dump.c), so it looked like too much
trouble to avoid 2 local variables here and 1 in vacuumdb.c. I don't
have a strong opinion here, so I can go for it if you prefer.Okay. This is a tad wider than the original patch proposal, and this
adds two lines. So let's discard that for now and keep it simple.
Ok!
+$node->issues_sql_like([qw(reindexdb -j2)], + qr/statement: REINDEX TABLE public.test1/, + 'Global and parallel reindex will issue per-table REINDEX'); Would it make sense to have some tests for schemas here?One of my comments in [1] has not been answered. What about
the decomposition of a list of schemas into a list of tables when
using the parallel mode?I did that in attached v6, and added suitable regression tests.
The two tests for "reindexdb -j2" can be combined into a single call,
checking for both commands to be generated in the same output. The
second command triggering a reindex on two schemas cannot be used to
check for the generation of both s1.t1 and s2.t2 as the ordering may
not be guaranteed. The commands arrays also looked inconsistent with
the rest. Attached is an updated patch with some format modifications
and the fixes for the tests.
Attached v8 based on your v7 + the fix for the dummy part.
Attachments:
reindex_parallel_v8.diffapplication/octet-stream; name=reindex_parallel_v8.diffDownload
diff --git a/doc/src/sgml/ref/reindexdb.sgml b/doc/src/sgml/ref/reindexdb.sgml
index 25b5a72770..477b77c6e9 100644
--- a/doc/src/sgml/ref/reindexdb.sgml
+++ b/doc/src/sgml/ref/reindexdb.sgml
@@ -166,6 +166,31 @@ PostgreSQL documentation
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>-j <replaceable class="parameter">njobs</replaceable></option></term>
+ <term><option>--jobs=<replaceable class="parameter">njobs</replaceable></option></term>
+ <listitem>
+ <para>
+ Execute the reindex commands in parallel by running
+ <replaceable class="parameter">njobs</replaceable>
+ commands simultaneously. This option reduces the time of the
+ processing but it also increases the load on the database server.
+ </para>
+ <para>
+ <application>reindexdb</application> will open
+ <replaceable class="parameter">njobs</replaceable> connections to the
+ database, so make sure your <xref linkend="guc-max-connections"/>
+ setting is high enough to accommodate all connections.
+ </para>
+ <para>
+ Note that this option is ignored with the <option>--index</option>
+ option to prevent deadlocks when processing multiple indexes from
+ the same relation, and incompatible with the <option>--system</option>
+ option.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>-q</option></term>
<term><option>--quiet</option></term>
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 3cd793b134..ede665090f 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -29,7 +29,7 @@ dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+reindexdb: reindexdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
install: all installdirs
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index 219a9a9211..ed46873bd2 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -10,10 +10,14 @@
*/
#include "postgres_fe.h"
+
+#include "catalog/pg_class_d.h"
#include "common.h"
#include "common/logging.h"
+#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
typedef enum ReindexType
{
@@ -25,16 +29,26 @@ typedef enum ReindexType
} ReindexType;
-static void reindex_one_database(const char *name, const char *dbname,
- ReindexType type, const char *host,
+static SimpleStringList *get_parallel_object_list(PGconn *conn,
+ ReindexType type,
+ SimpleStringList *user_list,
+ bool echo);
+static void reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
const char *port, const char *username,
enum trivalue prompt_password, const char *progname,
- bool echo, bool verbose, bool concurrently);
+ bool echo, bool verbose, bool concurrently,
+ int concurrentCons);
static void reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo,
- bool quiet, bool verbose, bool concurrently);
+ bool quiet, bool verbose, bool concurrently,
+ int concurrentCons);
+static void run_reindex_command(PGconn *conn, ReindexType type,
+ const char *name, bool echo, bool verbose,
+ bool concurrently, bool async);
+
static void help(const char *progname);
int
@@ -54,6 +68,7 @@ main(int argc, char *argv[])
{"system", no_argument, NULL, 's'},
{"table", required_argument, NULL, 't'},
{"index", required_argument, NULL, 'i'},
+ {"jobs", required_argument, NULL, 'j'},
{"verbose", no_argument, NULL, 'v'},
{"concurrently", no_argument, NULL, 1},
{"maintenance-db", required_argument, NULL, 2},
@@ -79,6 +94,9 @@ main(int argc, char *argv[])
SimpleStringList indexes = {NULL, NULL};
SimpleStringList tables = {NULL, NULL};
SimpleStringList schemas = {NULL, NULL};
+ int concurrentCons = 1;
+ int tbl_count = 0,
+ nsp_count = 0;
pg_logging_init(argv[0]);
progname = get_progname(argv[0]);
@@ -87,7 +105,7 @@ main(int argc, char *argv[])
handle_help_version_opts(argc, argv, "reindexdb", help);
/* process command-line options */
- while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:v", long_options, &optindex)) != -1)
+ while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:j:v", long_options, &optindex)) != -1)
{
switch (c)
{
@@ -114,6 +132,7 @@ main(int argc, char *argv[])
break;
case 'S':
simple_string_list_append(&schemas, optarg);
+ nsp_count++;
break;
case 'd':
dbname = pg_strdup(optarg);
@@ -126,10 +145,25 @@ main(int argc, char *argv[])
break;
case 't':
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
case 'i':
simple_string_list_append(&indexes, optarg);
break;
+ case 'j':
+ concurrentCons = atoi(optarg);
+ if (concurrentCons <= 0)
+ {
+ pg_log_error("number of parallel jobs must be at least 1");
+ exit(1);
+ }
+ if (concurrentCons > FD_SETSIZE - 1)
+ {
+ pg_log_error("too many parallel jobs requested (maximum: %d)",
+ FD_SETSIZE - 1);
+ exit(1);
+ }
+ break;
case 'v':
verbose = true;
break;
@@ -194,7 +228,8 @@ main(int argc, char *argv[])
}
reindex_all_databases(maintenance_db, host, port, username,
- prompt_password, progname, echo, quiet, verbose, concurrently);
+ prompt_password, progname, echo, quiet, verbose,
+ concurrently, concurrentCons);
}
else if (syscatalog)
{
@@ -214,6 +249,12 @@ main(int argc, char *argv[])
exit(1);
}
+ if (concurrentCons > 1)
+ {
+ pg_log_error("cannot use multiple jobs to reindex system catalogs");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -224,9 +265,9 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- reindex_one_database(NULL, dbname, REINDEX_SYSTEM, host,
+ reindex_one_database(dbname, REINDEX_SYSTEM, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, 1);
}
else
{
@@ -241,61 +282,57 @@ main(int argc, char *argv[])
}
if (schemas.head != NULL)
- {
- SimpleStringListCell *cell;
-
- for (cell = schemas.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_SCHEMA, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, nsp_count));
if (indexes.head != NULL)
{
- SimpleStringListCell *cell;
-
- for (cell = indexes.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_INDEX, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
+ /*
+ * An index list cannot be processed by multiple connections, as
+ * it could cause conflicts if reindexing multiple indexes from
+ * the same table. We simply ignore the passed number of jobs if
+ * any.
+ */
+ reindex_one_database(dbname, REINDEX_INDEX, &indexes, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently, 1);
}
- if (tables.head != NULL)
- {
- SimpleStringListCell *cell;
- for (cell = tables.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_TABLE, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ if (tables.head != NULL)
+ reindex_one_database(dbname, REINDEX_TABLE, &tables, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, tbl_count));
/*
* reindex database only if neither index nor table nor schema is
* specified
*/
if (indexes.head == NULL && tables.head == NULL && schemas.head == NULL)
- reindex_one_database(NULL, dbname, REINDEX_DATABASE, host,
+ reindex_one_database(dbname, REINDEX_DATABASE, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, concurrentCons);
}
exit(0);
}
static void
-reindex_one_database(const char *name, const char *dbname, ReindexType type,
- const char *host, const char *port, const char *username,
+reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
+ const char *port, const char *username,
enum trivalue prompt_password, const char *progname, bool echo,
- bool verbose, bool concurrently)
+ bool verbose, bool concurrently, int concurrentCons)
{
- PQExpBufferData sql;
PGconn *conn;
+ SimpleStringListCell *cell;
+ bool parallel = concurrentCons > 1;
+ SimpleStringList *process_list = user_list;
+ ReindexType process_type = type;
+ ParallelSlot *slots;
+ bool failed = false;
conn = connectDatabase(dbname, host, port, username, prompt_password,
progname, echo, false, false);
@@ -308,6 +345,107 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
exit(1);
}
+ if (!parallel)
+ {
+ if (user_list == NULL)
+ {
+ process_list = pg_malloc0(sizeof(SimpleStringList));
+ simple_string_list_append(process_list, PQdb(conn));
+ }
+ }
+ else
+ {
+ /*
+ * Database-wide parallel reindex requires special processing. If
+ * multiple jobs were asked, we have to reindex system catalogs first,
+ * as they can't be processed in parallel.
+ */
+ if (process_type == REINDEX_DATABASE)
+ {
+ Assert(user_list == NULL);
+ run_reindex_command(conn, REINDEX_SYSTEM, PQdb(conn), echo, verbose,
+ concurrently, false);
+
+ process_list = get_parallel_object_list(conn, process_type,
+ user_list, echo);
+ process_type = REINDEX_TABLE;
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ }
+ else if (process_type == REINDEX_SCHEMA)
+ {
+ Assert(user_list != NULL);
+
+ process_list = get_parallel_object_list(conn, process_type,
+ user_list, echo);
+ process_type = REINDEX_TABLE;
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ }
+ else
+ Assert(user_list != NULL);
+ }
+
+ Assert(process_list != NULL);
+
+ slots = ParallelSlotsSetup(dbname, host, port, username, prompt_password,
+ progname, echo, conn, concurrentCons);
+
+ cell = process_list->head;
+ do
+ {
+ const char *objname = cell->val;
+ ParallelSlot *free_slot = NULL;
+
+ if (CancelRequested)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ free_slot = ParallelSlotsGetIdle(slots, concurrentCons);
+ if (!free_slot)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ run_reindex_command(free_slot->connection, process_type, objname,
+ echo, verbose, concurrently, true);
+
+ cell = cell->next;
+ } while (cell != NULL);
+
+ if (!ParallelSlotsWaitCompletion(slots, concurrentCons))
+ failed = true;
+
+finish:
+ ParallelSlotsTerminate(slots, concurrentCons);
+ pfree(slots);
+
+ if (failed)
+ exit(1);
+}
+
+static void
+run_reindex_command(PGconn *conn, ReindexType type, const char *name,
+ bool echo, bool verbose, bool concurrently, bool async)
+{
+ PQExpBufferData sql;
+ bool status;
+
+ Assert(name);
+
/* build the REINDEX query */
initPQExpBuffer(&sql);
@@ -344,7 +482,7 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
{
case REINDEX_DATABASE:
case REINDEX_SYSTEM:
- appendPQExpBufferStr(&sql, fmtId(PQdb(conn)));
+ appendPQExpBufferStr(&sql, fmtId(name));
break;
case REINDEX_INDEX:
case REINDEX_TABLE:
@@ -358,7 +496,17 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
/* finish the query */
appendPQExpBufferChar(&sql, ';');
- if (!executeMaintenanceCommand(conn, sql.data, echo))
+ if (async)
+ {
+ if (echo)
+ printf("%s\n", sql.data);
+
+ status = PQsendQuery(conn, sql.data) == 1;
+ }
+ else
+ status = executeMaintenanceCommand(conn, sql.data, echo);
+
+ if (!status)
{
switch (type)
{
@@ -383,20 +531,135 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
name, PQdb(conn), PQerrorMessage(conn));
break;
}
- PQfinish(conn);
- exit(1);
+ if (!async)
+ {
+ PQfinish(conn);
+ exit(1);
+ }
}
- PQfinish(conn);
termPQExpBuffer(&sql);
}
+/*
+ * Prepare the list of objects to process by querying the catalogs.
+ *
+ * This function will return a SimpleStringList object containing the entire
+ * list of tables in the given database that should be processed by a parallel
+ * database-wide reindex (excluding system tables), or NULL if there's no such
+ * table.
+ */
+static SimpleStringList *
+get_parallel_object_list(PGconn *conn, ReindexType type,
+ SimpleStringList *user_list, bool echo)
+{
+ PQExpBufferData catalog_query;
+ PQExpBufferData buf;
+ PGresult *res;
+ SimpleStringList *tables;
+ int ntups,
+ i;
+
+ initPQExpBuffer(&catalog_query);
+
+ /*
+ * The queries here are using a safe search_path, so there's no need to
+ * fully qualify everything.
+ */
+ if (type == REINDEX_DATABASE)
+ {
+ Assert(user_list == NULL);
+
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE ns.nspname != 'pg_catalog'\n"
+ " AND c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " ORDER BY c.relpages DESC;");
+ }
+ else if (type == REINDEX_SCHEMA)
+ {
+ SimpleStringListCell *cell;
+ bool nsp_listed = false;
+
+ Assert(user_list != NULL);
+
+ /*
+ * All the tables from all the listed schemas are grabbed at
+ * once.
+ */
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " AND ns.nspname IN (");
+
+ for (cell = user_list->head; cell; cell = cell->next)
+ {
+ const char *nspname = cell->val;
+
+ if (nsp_listed)
+ appendPQExpBuffer(&catalog_query, ", ");
+ else
+ nsp_listed = true;
+
+ appendStringLiteralConn(&catalog_query, nspname, conn);
+
+ }
+
+ appendPQExpBuffer(&catalog_query, ")\n"
+ " ORDER BY c.relpages DESC;");
+ }
+ else
+ Assert(false);
+
+ res = executeQuery(conn, catalog_query.data, echo);
+ termPQExpBuffer(&catalog_query);
+
+ /*
+ * If no rows are returned, there are no matching tables, so we are done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return NULL;
+ }
+
+ tables = pg_malloc0(sizeof(SimpleStringList));
+
+ /* Build qualified identifiers for each table */
+ initPQExpBuffer(&buf);
+ for (i = 0; i < ntups; i++)
+ {
+ appendPQExpBufferStr(&buf,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+
+ simple_string_list_append(tables, buf.data);
+ resetPQExpBuffer(&buf);
+ }
+ termPQExpBuffer(&buf);
+ PQclear(res);
+
+ return tables;
+}
+
static void
reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo, bool quiet, bool verbose,
- bool concurrently)
+ bool concurrently, int concurrentCons)
{
PGconn *conn;
PGresult *result;
@@ -423,9 +686,10 @@ reindex_all_databases(const char *maintenance_db,
appendPQExpBufferStr(&connstr, "dbname=");
appendConnStrVal(&connstr, dbname);
- reindex_one_database(NULL, connstr.data, REINDEX_DATABASE, host,
+ reindex_one_database(connstr.data, REINDEX_DATABASE, NULL, host,
port, username, prompt_password,
- progname, echo, verbose, concurrently);
+ progname, echo, verbose, concurrently,
+ concurrentCons);
}
termPQExpBuffer(&connstr);
@@ -444,6 +708,7 @@ help(const char *progname)
printf(_(" -d, --dbname=DBNAME database to reindex\n"));
printf(_(" -e, --echo show the commands being sent to the server\n"));
printf(_(" -i, --index=INDEX recreate specific index(es) only\n"));
+ printf(_(" -j, --jobs=NUM use this many concurrent connections to reindex\n"));
printf(_(" -q, --quiet don't write any messages\n"));
printf(_(" -s, --system reindex system catalogs\n"));
printf(_(" -S, --schema=SCHEMA reindex specific schema(s) only\n"));
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index 1af8ab70ad..d058690892 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -3,7 +3,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 34;
+use Test::More tests => 39;
program_help_ok('reindexdb');
program_version_ok('reindexdb');
@@ -77,3 +77,27 @@ $node->command_ok(
$node->command_ok(
[qw(reindexdb --echo --system dbname=template1)],
'reindexdb system with connection string');
+
+# parallel processing
+$node->safe_psql(
+ 'postgres', q|
+ CREATE SCHEMA s1;
+ CREATE TABLE s1.t1(id integer);
+ CREATE INDEX ON s1.t1(id);
+ CREATE SCHEMA s2;
+ CREATE TABLE s2.t2(id integer);
+ CREATE INDEX ON s2.t2(id);
+|);
+
+$node->command_fails(
+ [ 'reindexdb', '-j', '2', '-s', 'postgres' ],
+ 'reindexdb cannot process system catalogs in parallel');
+$node->issues_sql_like(
+ [ 'reindexdb', '-j', '2', 'postgres' ],
+ qr/statement:\ REINDEX SYSTEM postgres;
+.*statement:\ REINDEX TABLE public\.test1/s,
+ 'global and parallel reindex issues REINDEX SYSTEM');
+$node->issues_sql_like(
+ [ 'reindexdb', '-j', '2', '-S', 's1', '-S', 's2', 'postgres' ],
+ qr/statement:\ REINDEX TABLE s1.t1;/,
+ 'Schema-specific and parallel reindex will issue per-table REINDEX');
The following review has been posted through the commitfest application:
make installcheck-world: tested, passed
Implements feature: tested, failed
Spec compliant: not tested
Documentation: tested, passed
Hi
I did some review and have few notes about behavior.
reindex database does not work with concurrently option:
./inst/bin/reindexdb --echo -d postgres -j 8 --concurrently
SELECT pg_catalog.set_config('search_path', '', false);
REINDEX SYSTEM CONCURRENTLY postgres;
reindexdb: error: reindexing of system catalogs on database "postgres" failed: ERROR: cannot reindex system catalogs concurrently
I think we need print message and skip system catalogs for concurrently reindex.
Or we can disallow concurrently database reindex with multiple jobs. I prefer first option.
+ reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host, + port, username, prompt_password, progname, + echo, verbose, concurrently, + Min(concurrentCons, nsp_count));
Should be just concurrentCons instead of Min(concurrentCons, nsp_count)
reindex_one_database for REINDEX_SCHEMA will build tables list and then processing by available workers. So:
-j 8 -S public -S public -S public -S poblic -S public -S public - will work with 6 jobs (and without multiple processing for same table)
-j 8 -S public - will have only one worker regardless tables count
if (concurrentCons > FD_SETSIZE - 1)
"if (concurrentCons >= FD_SETSIZE)" would not cleaner? Well, pgbench uses >= condition, vacuumdb uses > FD_SETSIZE - 1. No more FD_SETSIZE in conditions =)
regards, Sergei
Thanks for the review!
On Thu, Jul 25, 2019 at 10:17 AM Sergei Kornilov <sk@zsrv.org> wrote:
The following review has been posted through the commitfest application:
make installcheck-world: tested, passed
Implements feature: tested, failed
Spec compliant: not tested
Documentation: tested, passedHi
I did some review and have few notes about behavior.
reindex database does not work with concurrently option:
./inst/bin/reindexdb --echo -d postgres -j 8 --concurrently
SELECT pg_catalog.set_config('search_path', '', false);
REINDEX SYSTEM CONCURRENTLY postgres;
reindexdb: error: reindexing of system catalogs on database "postgres" failed: ERROR: cannot reindex system catalogs concurrentlyI think we need print message and skip system catalogs for concurrently reindex.
Or we can disallow concurrently database reindex with multiple jobs. I prefer first option.
Good point. I agree with 1st option, as that's already what would
happen without the --jobs switch:
$ reindexdb -d postgres --concurrently
WARNING: cannot reindex system catalogs concurrently, skipping all
(although this is emitted by the backend)
I modified the client code to behave the same and added a regression test.
+ reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host, + port, username, prompt_password, progname, + echo, verbose, concurrently, + Min(concurrentCons, nsp_count));Should be just concurrentCons instead of Min(concurrentCons, nsp_count)
Indeed, that changed with v8 and I forgot to update it, fixed.
reindex_one_database for REINDEX_SCHEMA will build tables list and then processing by available workers. So:
-j 8 -S public -S public -S public -S poblic -S public -S public - will work with 6 jobs (and without multiple processing for same table)
-j 8 -S public - will have only one worker regardless tables countif (concurrentCons > FD_SETSIZE - 1)
"if (concurrentCons >= FD_SETSIZE)" would not cleaner? Well, pgbench uses >= condition, vacuumdb uses > FD_SETSIZE - 1. No more FD_SETSIZE in conditions =)
I don't have a strong opinion here. If we change for >=, it'd be
better to also adapt vacuumdb for consistency. I didn't change it for
now, to stay consistent with vacuumdb.
Attachments:
reindex_parallel_v9.diffapplication/octet-stream; name=reindex_parallel_v9.diffDownload
diff --git a/doc/src/sgml/ref/reindexdb.sgml b/doc/src/sgml/ref/reindexdb.sgml
index 25b5a72770..477b77c6e9 100644
--- a/doc/src/sgml/ref/reindexdb.sgml
+++ b/doc/src/sgml/ref/reindexdb.sgml
@@ -166,6 +166,31 @@ PostgreSQL documentation
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>-j <replaceable class="parameter">njobs</replaceable></option></term>
+ <term><option>--jobs=<replaceable class="parameter">njobs</replaceable></option></term>
+ <listitem>
+ <para>
+ Execute the reindex commands in parallel by running
+ <replaceable class="parameter">njobs</replaceable>
+ commands simultaneously. This option reduces the time of the
+ processing but it also increases the load on the database server.
+ </para>
+ <para>
+ <application>reindexdb</application> will open
+ <replaceable class="parameter">njobs</replaceable> connections to the
+ database, so make sure your <xref linkend="guc-max-connections"/>
+ setting is high enough to accommodate all connections.
+ </para>
+ <para>
+ Note that this option is ignored with the <option>--index</option>
+ option to prevent deadlocks when processing multiple indexes from
+ the same relation, and incompatible with the <option>--system</option>
+ option.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>-q</option></term>
<term><option>--quiet</option></term>
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 3cd793b134..ede665090f 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -29,7 +29,7 @@ dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+reindexdb: reindexdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
install: all installdirs
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index 219a9a9211..39b001cf25 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -10,10 +10,14 @@
*/
#include "postgres_fe.h"
+
+#include "catalog/pg_class_d.h"
#include "common.h"
#include "common/logging.h"
+#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
typedef enum ReindexType
{
@@ -25,16 +29,26 @@ typedef enum ReindexType
} ReindexType;
-static void reindex_one_database(const char *name, const char *dbname,
- ReindexType type, const char *host,
+static SimpleStringList *get_parallel_object_list(PGconn *conn,
+ ReindexType type,
+ SimpleStringList *user_list,
+ bool echo);
+static void reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
const char *port, const char *username,
enum trivalue prompt_password, const char *progname,
- bool echo, bool verbose, bool concurrently);
+ bool echo, bool verbose, bool concurrently,
+ int concurrentCons);
static void reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo,
- bool quiet, bool verbose, bool concurrently);
+ bool quiet, bool verbose, bool concurrently,
+ int concurrentCons);
+static void run_reindex_command(PGconn *conn, ReindexType type,
+ const char *name, bool echo, bool verbose,
+ bool concurrently, bool async);
+
static void help(const char *progname);
int
@@ -54,6 +68,7 @@ main(int argc, char *argv[])
{"system", no_argument, NULL, 's'},
{"table", required_argument, NULL, 't'},
{"index", required_argument, NULL, 'i'},
+ {"jobs", required_argument, NULL, 'j'},
{"verbose", no_argument, NULL, 'v'},
{"concurrently", no_argument, NULL, 1},
{"maintenance-db", required_argument, NULL, 2},
@@ -79,6 +94,8 @@ main(int argc, char *argv[])
SimpleStringList indexes = {NULL, NULL};
SimpleStringList tables = {NULL, NULL};
SimpleStringList schemas = {NULL, NULL};
+ int concurrentCons = 1;
+ int tbl_count = 0;
pg_logging_init(argv[0]);
progname = get_progname(argv[0]);
@@ -87,7 +104,7 @@ main(int argc, char *argv[])
handle_help_version_opts(argc, argv, "reindexdb", help);
/* process command-line options */
- while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:v", long_options, &optindex)) != -1)
+ while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:j:v", long_options, &optindex)) != -1)
{
switch (c)
{
@@ -126,10 +143,25 @@ main(int argc, char *argv[])
break;
case 't':
simple_string_list_append(&tables, optarg);
+ tbl_count++;
break;
case 'i':
simple_string_list_append(&indexes, optarg);
break;
+ case 'j':
+ concurrentCons = atoi(optarg);
+ if (concurrentCons <= 0)
+ {
+ pg_log_error("number of parallel jobs must be at least 1");
+ exit(1);
+ }
+ if (concurrentCons > FD_SETSIZE - 1)
+ {
+ pg_log_error("too many parallel jobs requested (maximum: %d)",
+ FD_SETSIZE - 1);
+ exit(1);
+ }
+ break;
case 'v':
verbose = true;
break;
@@ -194,7 +226,8 @@ main(int argc, char *argv[])
}
reindex_all_databases(maintenance_db, host, port, username,
- prompt_password, progname, echo, quiet, verbose, concurrently);
+ prompt_password, progname, echo, quiet, verbose,
+ concurrently, concurrentCons);
}
else if (syscatalog)
{
@@ -214,6 +247,12 @@ main(int argc, char *argv[])
exit(1);
}
+ if (concurrentCons > 1)
+ {
+ pg_log_error("cannot use multiple jobs to reindex system catalogs");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -224,9 +263,9 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- reindex_one_database(NULL, dbname, REINDEX_SYSTEM, host,
+ reindex_one_database(dbname, REINDEX_SYSTEM, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, 1);
}
else
{
@@ -241,61 +280,56 @@ main(int argc, char *argv[])
}
if (schemas.head != NULL)
- {
- SimpleStringListCell *cell;
-
- for (cell = schemas.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_SCHEMA, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently, concurrentCons);
if (indexes.head != NULL)
{
- SimpleStringListCell *cell;
-
- for (cell = indexes.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_INDEX, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
+ /*
+ * An index list cannot be processed by multiple connections, as
+ * it could cause conflicts if reindexing multiple indexes from
+ * the same table. We simply ignore the passed number of jobs if
+ * any.
+ */
+ reindex_one_database(dbname, REINDEX_INDEX, &indexes, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently, 1);
}
- if (tables.head != NULL)
- {
- SimpleStringListCell *cell;
- for (cell = tables.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_TABLE, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ if (tables.head != NULL)
+ reindex_one_database(dbname, REINDEX_TABLE, &tables, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ Min(concurrentCons, tbl_count));
/*
* reindex database only if neither index nor table nor schema is
* specified
*/
if (indexes.head == NULL && tables.head == NULL && schemas.head == NULL)
- reindex_one_database(NULL, dbname, REINDEX_DATABASE, host,
+ reindex_one_database(dbname, REINDEX_DATABASE, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, concurrentCons);
}
exit(0);
}
static void
-reindex_one_database(const char *name, const char *dbname, ReindexType type,
- const char *host, const char *port, const char *username,
+reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
+ const char *port, const char *username,
enum trivalue prompt_password, const char *progname, bool echo,
- bool verbose, bool concurrently)
+ bool verbose, bool concurrently, int concurrentCons)
{
- PQExpBufferData sql;
PGconn *conn;
+ SimpleStringListCell *cell;
+ bool parallel = concurrentCons > 1;
+ SimpleStringList *process_list = user_list;
+ ReindexType process_type = type;
+ ParallelSlot *slots;
+ bool failed = false;
conn = connectDatabase(dbname, host, port, username, prompt_password,
progname, echo, false, false);
@@ -308,6 +342,110 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
exit(1);
}
+ if (!parallel)
+ {
+ if (user_list == NULL)
+ {
+ process_list = pg_malloc0(sizeof(SimpleStringList));
+ simple_string_list_append(process_list, PQdb(conn));
+ }
+ }
+ else
+ {
+ /*
+ * Database-wide parallel reindex requires special processing. If
+ * multiple jobs were asked, we have to reindex system catalogs first,
+ * as they can't be processed in parallel.
+ */
+ if (process_type == REINDEX_DATABASE)
+ {
+ Assert(user_list == NULL);
+ if (concurrently)
+ pg_log_warning("cannot reindex system catalogs concurrently, skipping all");
+ else
+ run_reindex_command(conn, REINDEX_SYSTEM, PQdb(conn), echo,
+ verbose, concurrently, false);
+
+ process_list = get_parallel_object_list(conn, process_type,
+ user_list, echo);
+ process_type = REINDEX_TABLE;
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ }
+ else if (process_type == REINDEX_SCHEMA)
+ {
+ Assert(user_list != NULL);
+
+ process_list = get_parallel_object_list(conn, process_type,
+ user_list, echo);
+ process_type = REINDEX_TABLE;
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ }
+ else
+ Assert(user_list != NULL);
+ }
+
+ Assert(process_list != NULL);
+
+ slots = ParallelSlotsSetup(dbname, host, port, username, prompt_password,
+ progname, echo, conn, concurrentCons);
+
+ cell = process_list->head;
+ do
+ {
+ const char *objname = cell->val;
+ ParallelSlot *free_slot = NULL;
+
+ if (CancelRequested)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ free_slot = ParallelSlotsGetIdle(slots, concurrentCons);
+ if (!free_slot)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ run_reindex_command(free_slot->connection, process_type, objname,
+ echo, verbose, concurrently, true);
+
+ cell = cell->next;
+ } while (cell != NULL);
+
+ if (!ParallelSlotsWaitCompletion(slots, concurrentCons))
+ failed = true;
+
+finish:
+ ParallelSlotsTerminate(slots, concurrentCons);
+ pfree(slots);
+
+ if (failed)
+ exit(1);
+}
+
+static void
+run_reindex_command(PGconn *conn, ReindexType type, const char *name,
+ bool echo, bool verbose, bool concurrently, bool async)
+{
+ PQExpBufferData sql;
+ bool status;
+
+ Assert(name);
+
/* build the REINDEX query */
initPQExpBuffer(&sql);
@@ -344,7 +482,7 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
{
case REINDEX_DATABASE:
case REINDEX_SYSTEM:
- appendPQExpBufferStr(&sql, fmtId(PQdb(conn)));
+ appendPQExpBufferStr(&sql, fmtId(name));
break;
case REINDEX_INDEX:
case REINDEX_TABLE:
@@ -358,7 +496,17 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
/* finish the query */
appendPQExpBufferChar(&sql, ';');
- if (!executeMaintenanceCommand(conn, sql.data, echo))
+ if (async)
+ {
+ if (echo)
+ printf("%s\n", sql.data);
+
+ status = PQsendQuery(conn, sql.data) == 1;
+ }
+ else
+ status = executeMaintenanceCommand(conn, sql.data, echo);
+
+ if (!status)
{
switch (type)
{
@@ -383,20 +531,135 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
name, PQdb(conn), PQerrorMessage(conn));
break;
}
- PQfinish(conn);
- exit(1);
+ if (!async)
+ {
+ PQfinish(conn);
+ exit(1);
+ }
}
- PQfinish(conn);
termPQExpBuffer(&sql);
}
+/*
+ * Prepare the list of objects to process by querying the catalogs.
+ *
+ * This function will return a SimpleStringList object containing the entire
+ * list of tables in the given database that should be processed by a parallel
+ * database-wide reindex (excluding system tables), or NULL if there's no such
+ * table.
+ */
+static SimpleStringList *
+get_parallel_object_list(PGconn *conn, ReindexType type,
+ SimpleStringList *user_list, bool echo)
+{
+ PQExpBufferData catalog_query;
+ PQExpBufferData buf;
+ PGresult *res;
+ SimpleStringList *tables;
+ int ntups,
+ i;
+
+ initPQExpBuffer(&catalog_query);
+
+ /*
+ * The queries here are using a safe search_path, so there's no need to
+ * fully qualify everything.
+ */
+ if (type == REINDEX_DATABASE)
+ {
+ Assert(user_list == NULL);
+
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE ns.nspname != 'pg_catalog'\n"
+ " AND c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " ORDER BY c.relpages DESC;");
+ }
+ else if (type == REINDEX_SCHEMA)
+ {
+ SimpleStringListCell *cell;
+ bool nsp_listed = false;
+
+ Assert(user_list != NULL);
+
+ /*
+ * All the tables from all the listed schemas are grabbed at
+ * once.
+ */
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " AND ns.nspname IN (");
+
+ for (cell = user_list->head; cell; cell = cell->next)
+ {
+ const char *nspname = cell->val;
+
+ if (nsp_listed)
+ appendPQExpBuffer(&catalog_query, ", ");
+ else
+ nsp_listed = true;
+
+ appendStringLiteralConn(&catalog_query, nspname, conn);
+
+ }
+
+ appendPQExpBuffer(&catalog_query, ")\n"
+ " ORDER BY c.relpages DESC;");
+ }
+ else
+ Assert(false);
+
+ res = executeQuery(conn, catalog_query.data, echo);
+ termPQExpBuffer(&catalog_query);
+
+ /*
+ * If no rows are returned, there are no matching tables, so we are done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return NULL;
+ }
+
+ tables = pg_malloc0(sizeof(SimpleStringList));
+
+ /* Build qualified identifiers for each table */
+ initPQExpBuffer(&buf);
+ for (i = 0; i < ntups; i++)
+ {
+ appendPQExpBufferStr(&buf,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+
+ simple_string_list_append(tables, buf.data);
+ resetPQExpBuffer(&buf);
+ }
+ termPQExpBuffer(&buf);
+ PQclear(res);
+
+ return tables;
+}
+
static void
reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo, bool quiet, bool verbose,
- bool concurrently)
+ bool concurrently, int concurrentCons)
{
PGconn *conn;
PGresult *result;
@@ -423,9 +686,10 @@ reindex_all_databases(const char *maintenance_db,
appendPQExpBufferStr(&connstr, "dbname=");
appendConnStrVal(&connstr, dbname);
- reindex_one_database(NULL, connstr.data, REINDEX_DATABASE, host,
+ reindex_one_database(connstr.data, REINDEX_DATABASE, NULL, host,
port, username, prompt_password,
- progname, echo, verbose, concurrently);
+ progname, echo, verbose, concurrently,
+ concurrentCons);
}
termPQExpBuffer(&connstr);
@@ -444,6 +708,7 @@ help(const char *progname)
printf(_(" -d, --dbname=DBNAME database to reindex\n"));
printf(_(" -e, --echo show the commands being sent to the server\n"));
printf(_(" -i, --index=INDEX recreate specific index(es) only\n"));
+ printf(_(" -j, --jobs=NUM use this many concurrent connections to reindex\n"));
printf(_(" -q, --quiet don't write any messages\n"));
printf(_(" -s, --system reindex system catalogs\n"));
printf(_(" -S, --schema=SCHEMA reindex specific schema(s) only\n"));
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index 1af8ab70ad..2bc41b2f05 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -3,7 +3,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 34;
+use Test::More tests => 42;
program_help_ok('reindexdb');
program_version_ok('reindexdb');
@@ -77,3 +77,33 @@ $node->command_ok(
$node->command_ok(
[qw(reindexdb --echo --system dbname=template1)],
'reindexdb system with connection string');
+
+# parallel processing
+$node->safe_psql(
+ 'postgres', q|
+ CREATE SCHEMA s1;
+ CREATE TABLE s1.t1(id integer);
+ CREATE INDEX ON s1.t1(id);
+ CREATE SCHEMA s2;
+ CREATE TABLE s2.t2(id integer);
+ CREATE INDEX ON s2.t2(id);
+|);
+
+$node->command_fails(
+ [ 'reindexdb', '-j', '2', '-s', 'postgres' ],
+ 'reindexdb cannot process system catalogs in parallel');
+$node->issues_sql_like(
+ [ 'reindexdb', '-j', '2', 'postgres' ],
+ qr/statement:\ REINDEX SYSTEM postgres;
+.*statement:\ REINDEX TABLE public\.test1/s,
+ 'global and parallel reindex issues REINDEX SYSTEM');
+$node->issues_sql_like(
+ [ 'reindexdb', '-j', '2', '-S', 's1', '-S', 's2', 'postgres' ],
+ qr/statement:\ REINDEX TABLE s1.t1;/,
+ 'Schema-specific and parallel reindex will issue per-table REINDEX');
+$node->command_checks_all(
+ [ 'reindexdb', '-j2', '--concurrently', '-d', 'postgres' ],
+ 0,
+ [qr/^$/],
+ [qr/^reindexdb: warning: cannot reindex system catalogs concurrently, skipping all/s],
+ 'Parallel, concurrent and global reindex does not process system catalogs');
Hi
Thank you, v9 code and behavior looks good for me. Builds cleanly, works with older releases. I'll mark Ready to Commiter.
I don't have a strong opinion here. If we change for >=, it'd be
better to also adapt vacuumdb for consistency. I didn't change it for
now, to stay consistent with vacuumdb.
Yep, no strong opinion from me too.
regards, Sergei
On Thu, Jul 25, 2019 at 12:12:40PM +0300, Sergei Kornilov wrote:
Thank you, v9 code and behavior looks good for me. Builds cleanly,
works with older releases. I'll mark Ready to Commiter.
The restriction with --jobs and --system is not documented, and that's
good to have from the start. This actually makes the parallel job
handling with --index inconsistent as we mask parallelism in this case
by enforcing the use of one connection. I think that we should
revisit the interactions with --index and --jobs actually, because,
assuming that users never read the documentation, they may actually be
surprised to see that something like --index idx1 .. --index idxN
--jobs=N does not lead to any improvements at all, until they find out
the reason why. It is also much easier to have an error as starting
point because it can be lifted later one. There is an argument that
we may actually not have this restriction at all on --index as long as
the user knows what it is doing and does not define indexes from the
same relation, still I would keep an error.
Thinking deeper about it, there is also a point of gathering first all
the relations if one associates --schemas and --tables in the same
call of reindexdb and then pass down a list of decomposed relations
which are processed in parallel. The code as currently presented is
rather straight-forward, and I don't think that this is worth the
extra complication, but this was not mentioned until now on this
thread :)
For the non-parallel case in reindex_one_database(), I would add an
Assert on REINDEX_DATABASE and REINDEX_SYSTEM with a comment to
mention that a NULL list of objects can just be passed down only in
those two cases when the single-object list with the database name is
built.
I don't have a strong opinion here. If we change for >=, it'd be
better to also adapt vacuumdb for consistency. I didn't change it for
now, to stay consistent with vacuumdb.Yep, no strong opinion from me too.
My opinion tends towards consistency. Consistency sounds good.
--
Michael
Hi
I don't have a strong opinion here. If we change for >=, it'd be
better to also adapt vacuumdb for consistency. I didn't change it for
now, to stay consistent with vacuumdb.Yep, no strong opinion from me too.
My opinion tends towards consistency. Consistency sounds good.
Which one consistency you prefer? Currently we have just one >= FD_SETSIZE in pgbench and one > FD_SETSIZE -1 in vacuumdb. That's all.
regards, Sergei
On Thu, Jul 25, 2019 at 12:03 PM Michael Paquier <michael@paquier.xyz> wrote:
On Thu, Jul 25, 2019 at 12:12:40PM +0300, Sergei Kornilov wrote:
Thank you, v9 code and behavior looks good for me. Builds cleanly,
works with older releases. I'll mark Ready to Commiter.The restriction with --jobs and --system is not documented, and that's
good to have from the start.
That's documented in the patch:
+ Note that this option is ignored with the <option>--index</option>
+ option to prevent deadlocks when processing multiple indexes from
+ the same relation, and incompatible with the <option>--system</option>
+ option.
The restriction with --jobs and --concurrently is indeed not
specifically documented in reindexdb.sgml, there's only a mention in
reindex.sgml:
<command>REINDEX SYSTEM</command> does not support
<command>CONCURRENTLY</command> since system catalogs cannot be reindexed
The behavior doesn't really change with this patch, though we could
enhance the documentation.
This actually makes the parallel job
handling with --index inconsistent as we mask parallelism in this case
by enforcing the use of one connection. I think that we should
revisit the interactions with --index and --jobs actually, because,
assuming that users never read the documentation, they may actually be
surprised to see that something like --index idx1 .. --index idxN
--jobs=N does not lead to any improvements at all, until they find out
the reason why.
The problem is that a user doing something like:
reindexdb -j48 -i some_index -S s1 -S s2 -S s3....
will probably be disappointed to learn that he has to run a specific
command for the index(es) that should be reindexed. Maybe we can
issue a warning that parallelism isn't used when an index list is
processed and user asked for multiple jobs?
Thinking deeper about it, there is also a point of gathering first all
the relations if one associates --schemas and --tables in the same
call of reindexdb and then pass down a list of decomposed relations
which are processed in parallel. The code as currently presented is
rather straight-forward, and I don't think that this is worth the
extra complication, but this was not mentioned until now on this
thread :)
+1
For the non-parallel case in reindex_one_database(), I would add an
Assert on REINDEX_DATABASE and REINDEX_SYSTEM with a comment to
mention that a NULL list of objects can just be passed down only in
those two cases when the single-object list with the database name is
built.
Something like that?
if (!parallel)
{
- if (user_list == NULL)
+ /*
+ * Database-wide and system catalogs processing should omit the list
+ * of objects to process.
+ */
+ if (process_type == REINDEX_DATABASE || process_type == REINDEX_SYSTEM)
{
+ Assert(user_list == NULL);
+
process_list = pg_malloc0(sizeof(SimpleStringList));
simple_string_list_append(process_list, PQdb(conn));
}
There's another assert after the else-parallel that checks that a
list is present, so there's no need to also check it here.
I don't send a new patch since the --index wanted behavior is not clear yet.
On Thu, Jul 25, 2019 at 01:00:34PM +0200, Julien Rouhaud wrote:
The problem is that a user doing something like:
reindexdb -j48 -i some_index -S s1 -S s2 -S s3....
will probably be disappointed to learn that he has to run a specific
command for the index(es) that should be reindexed. Maybe we can
issue a warning that parallelism isn't used when an index list is
processed and user asked for multiple jobs?
Arguments go in both directions as some other users may be surprised
by the performance of indexes as serialization is enforced.
I don't send a new patch since the --index wanted behavior is not
clear yet.
So I am sending one patch (actually two) after a closer review that I
have spent time shaping into a committable state. And for this part I
have another suggestion that is to use a switch/case without a default
so as any newly-added object types would allow somebody to think about
those code paths as this would generate compiler warnings.
While reviewing I have found an extra bug in the logic: when using a
list of tables, the number of parallel slots is the minimum between
concurrentCons and tbl_count, but this does not get applied after
building a list of tables for a schema or database reindex, so we had
better recompute the number of items in reindex_one_database() before
allocating the number of parallel slots. There was also a small gem
in the TAP tests for one of the commands using "-j2" in one of the
command arguments.
So here we go:
- 0001 is your original thing, with --jobs enforced to 1 for the index
part.
- 0002 is my addition to forbid --index with --jobs.
I am fine to be outvoted regarding 0002, and it is the case based on
the state of this thread with 2:1. We could always revisit this
decision in this development cycle anyway.
--
Michael
Attachments:
0001-Base-patch-for-support-of-jobs-in-reindexdb.patchtext/x-diff; charset=us-asciiDownload
From 77f24d9e3edb40b8ac2674230e10345988b8831c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 26 Jul 2019 12:12:46 +0900
Subject: [PATCH 1/2] Base patch for support of --jobs in reindexdb
---
doc/src/sgml/ref/reindexdb.sgml | 25 ++
src/bin/scripts/Makefile | 2 +-
src/bin/scripts/reindexdb.c | 407 +++++++++++++++++++++++++----
src/bin/scripts/t/090_reindexdb.pl | 32 ++-
4 files changed, 414 insertions(+), 52 deletions(-)
diff --git a/doc/src/sgml/ref/reindexdb.sgml b/doc/src/sgml/ref/reindexdb.sgml
index 25b5a72770..477b77c6e9 100644
--- a/doc/src/sgml/ref/reindexdb.sgml
+++ b/doc/src/sgml/ref/reindexdb.sgml
@@ -166,6 +166,31 @@ PostgreSQL documentation
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>-j <replaceable class="parameter">njobs</replaceable></option></term>
+ <term><option>--jobs=<replaceable class="parameter">njobs</replaceable></option></term>
+ <listitem>
+ <para>
+ Execute the reindex commands in parallel by running
+ <replaceable class="parameter">njobs</replaceable>
+ commands simultaneously. This option reduces the time of the
+ processing but it also increases the load on the database server.
+ </para>
+ <para>
+ <application>reindexdb</application> will open
+ <replaceable class="parameter">njobs</replaceable> connections to the
+ database, so make sure your <xref linkend="guc-max-connections"/>
+ setting is high enough to accommodate all connections.
+ </para>
+ <para>
+ Note that this option is ignored with the <option>--index</option>
+ option to prevent deadlocks when processing multiple indexes from
+ the same relation, and incompatible with the <option>--system</option>
+ option.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>-q</option></term>
<term><option>--quiet</option></term>
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 3cd793b134..ede665090f 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -29,7 +29,7 @@ dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-
dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
vacuumdb: vacuumdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+reindexdb: reindexdb.o common.o scripts_parallel.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
install: all installdirs
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index 219a9a9211..a877689eec 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -10,10 +10,14 @@
*/
#include "postgres_fe.h"
+
+#include "catalog/pg_class_d.h"
#include "common.h"
#include "common/logging.h"
+#include "fe_utils/connect.h"
#include "fe_utils/simple_list.h"
#include "fe_utils/string_utils.h"
+#include "scripts_parallel.h"
typedef enum ReindexType
{
@@ -25,16 +29,26 @@ typedef enum ReindexType
} ReindexType;
-static void reindex_one_database(const char *name, const char *dbname,
- ReindexType type, const char *host,
+static SimpleStringList *get_parallel_object_list(PGconn *conn,
+ ReindexType type,
+ SimpleStringList *user_list,
+ bool echo);
+static void reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
const char *port, const char *username,
enum trivalue prompt_password, const char *progname,
- bool echo, bool verbose, bool concurrently);
+ bool echo, bool verbose, bool concurrently,
+ int concurrentCons);
static void reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo,
- bool quiet, bool verbose, bool concurrently);
+ bool quiet, bool verbose, bool concurrently,
+ int concurrentCons);
+static void run_reindex_command(PGconn *conn, ReindexType type,
+ const char *name, bool echo, bool verbose,
+ bool concurrently, bool async);
+
static void help(const char *progname);
int
@@ -54,6 +68,7 @@ main(int argc, char *argv[])
{"system", no_argument, NULL, 's'},
{"table", required_argument, NULL, 't'},
{"index", required_argument, NULL, 'i'},
+ {"jobs", required_argument, NULL, 'j'},
{"verbose", no_argument, NULL, 'v'},
{"concurrently", no_argument, NULL, 1},
{"maintenance-db", required_argument, NULL, 2},
@@ -79,6 +94,7 @@ main(int argc, char *argv[])
SimpleStringList indexes = {NULL, NULL};
SimpleStringList tables = {NULL, NULL};
SimpleStringList schemas = {NULL, NULL};
+ int concurrentCons = 1;
pg_logging_init(argv[0]);
progname = get_progname(argv[0]);
@@ -87,7 +103,7 @@ main(int argc, char *argv[])
handle_help_version_opts(argc, argv, "reindexdb", help);
/* process command-line options */
- while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:v", long_options, &optindex)) != -1)
+ while ((c = getopt_long(argc, argv, "h:p:U:wWeqS:d:ast:i:j:v", long_options, &optindex)) != -1)
{
switch (c)
{
@@ -130,6 +146,20 @@ main(int argc, char *argv[])
case 'i':
simple_string_list_append(&indexes, optarg);
break;
+ case 'j':
+ concurrentCons = atoi(optarg);
+ if (concurrentCons <= 0)
+ {
+ pg_log_error("number of parallel jobs must be at least 1");
+ exit(1);
+ }
+ if (concurrentCons > FD_SETSIZE - 1)
+ {
+ pg_log_error("too many parallel jobs requested (maximum: %d)",
+ FD_SETSIZE - 1);
+ exit(1);
+ }
+ break;
case 'v':
verbose = true;
break;
@@ -194,7 +224,8 @@ main(int argc, char *argv[])
}
reindex_all_databases(maintenance_db, host, port, username,
- prompt_password, progname, echo, quiet, verbose, concurrently);
+ prompt_password, progname, echo, quiet, verbose,
+ concurrently, concurrentCons);
}
else if (syscatalog)
{
@@ -214,6 +245,12 @@ main(int argc, char *argv[])
exit(1);
}
+ if (concurrentCons > 1)
+ {
+ pg_log_error("cannot use multiple jobs to reindex system catalogs");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -224,9 +261,9 @@ main(int argc, char *argv[])
dbname = get_user_name_or_exit(progname);
}
- reindex_one_database(NULL, dbname, REINDEX_SYSTEM, host,
+ reindex_one_database(dbname, REINDEX_SYSTEM, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, 1);
}
else
{
@@ -241,61 +278,57 @@ main(int argc, char *argv[])
}
if (schemas.head != NULL)
- {
- SimpleStringListCell *cell;
-
- for (cell = schemas.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_SCHEMA, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ reindex_one_database(dbname, REINDEX_SCHEMA, &schemas, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently, concurrentCons);
if (indexes.head != NULL)
{
- SimpleStringListCell *cell;
-
- for (cell = indexes.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_INDEX, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
+ /*
+ * An index list cannot be processed by multiple connections, as
+ * it could cause conflicts if reindexing multiple indexes from
+ * the same table. We simply ignore the passed number of jobs if
+ * any.
+ */
+ reindex_one_database(dbname, REINDEX_INDEX, &indexes, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently, 1);
}
+
if (tables.head != NULL)
- {
- SimpleStringListCell *cell;
-
- for (cell = tables.head; cell; cell = cell->next)
- {
- reindex_one_database(cell->val, dbname, REINDEX_TABLE, host,
- port, username, prompt_password, progname,
- echo, verbose, concurrently);
- }
- }
+ reindex_one_database(dbname, REINDEX_TABLE, &tables, host,
+ port, username, prompt_password, progname,
+ echo, verbose, concurrently,
+ concurrentCons);
/*
* reindex database only if neither index nor table nor schema is
* specified
*/
if (indexes.head == NULL && tables.head == NULL && schemas.head == NULL)
- reindex_one_database(NULL, dbname, REINDEX_DATABASE, host,
+ reindex_one_database(dbname, REINDEX_DATABASE, NULL, host,
port, username, prompt_password, progname,
- echo, verbose, concurrently);
+ echo, verbose, concurrently, concurrentCons);
}
exit(0);
}
static void
-reindex_one_database(const char *name, const char *dbname, ReindexType type,
- const char *host, const char *port, const char *username,
+reindex_one_database(const char *dbname, ReindexType type,
+ SimpleStringList *user_list, const char *host,
+ const char *port, const char *username,
enum trivalue prompt_password, const char *progname, bool echo,
- bool verbose, bool concurrently)
+ bool verbose, bool concurrently, int concurrentCons)
{
- PQExpBufferData sql;
PGconn *conn;
+ SimpleStringListCell *cell;
+ bool parallel = concurrentCons > 1;
+ SimpleStringList *process_list = user_list;
+ ReindexType process_type = type;
+ ParallelSlot *slots;
+ bool failed = false;
+ int items_count = 0;
conn = connectDatabase(dbname, host, port, username, prompt_password,
progname, echo, false, false);
@@ -308,6 +341,148 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
exit(1);
}
+ if (!parallel)
+ {
+ switch (process_type)
+ {
+ case REINDEX_DATABASE:
+ case REINDEX_SYSTEM:
+
+ /*
+ * Database and system reindexes only need to work on the
+ * database itself, so build a list with a single entry.
+ */
+ Assert(user_list == NULL);
+ process_list = pg_malloc0(sizeof(SimpleStringList));
+ simple_string_list_append(process_list, PQdb(conn));
+ break;
+
+ case REINDEX_INDEX:
+ case REINDEX_SCHEMA:
+ case REINDEX_TABLE:
+ Assert(user_list != NULL);
+ break;
+ }
+ }
+ else
+ {
+ switch (process_type)
+ {
+ case REINDEX_DATABASE:
+
+ /*
+ * Database-wide parallel reindex requires special processing.
+ * If multiple jobs were asked, we have to reindex system
+ * catalogs first as they cannot be processed in parallel.
+ */
+ if (concurrently)
+ pg_log_warning("cannot reindex system catalogs concurrently, skipping all");
+ else
+ run_reindex_command(conn, REINDEX_SYSTEM, PQdb(conn), echo,
+ verbose, concurrently, false);
+
+ process_list = get_parallel_object_list(conn, process_type,
+ user_list, echo);
+ process_type = REINDEX_TABLE;
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ break;
+
+ case REINDEX_SCHEMA:
+ Assert(user_list != NULL);
+
+ process_list = get_parallel_object_list(conn, process_type,
+ user_list, echo);
+ process_type = REINDEX_TABLE;
+
+ /* Bail out if nothing to process */
+ if (process_list == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+ break;
+
+ case REINDEX_SYSTEM:
+ /* not supported */
+ Assert(false);
+ break;
+
+ case REINDEX_TABLE:
+ case REINDEX_INDEX:
+
+ /*
+ * Fall through. The list of items for indexes and tables is
+ * already created.
+ */
+ break;
+ }
+ }
+
+ /*
+ * Adjust the number of concurrent connections depending on the items in
+ * the list. We choose the minimum between the number of concurrent
+ * connections and the number of items in the list.
+ */
+ for (cell = process_list->head; cell; cell = cell->next)
+ items_count++;
+ concurrentCons = Min(concurrentCons, items_count);
+
+ Assert(process_list != NULL);
+
+ slots = ParallelSlotsSetup(dbname, host, port, username, prompt_password,
+ progname, echo, conn, concurrentCons);
+
+ cell = process_list->head;
+ do
+ {
+ const char *objname = cell->val;
+ ParallelSlot *free_slot = NULL;
+
+ if (CancelRequested)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ free_slot = ParallelSlotsGetIdle(slots, concurrentCons);
+ if (!free_slot)
+ {
+ failed = true;
+ goto finish;
+ }
+
+ run_reindex_command(free_slot->connection, process_type, objname,
+ echo, verbose, concurrently, true);
+
+ cell = cell->next;
+ } while (cell != NULL);
+
+ if (!ParallelSlotsWaitCompletion(slots, concurrentCons))
+ failed = true;
+
+finish:
+ ParallelSlotsTerminate(slots, concurrentCons);
+ pfree(slots);
+
+ if (failed)
+ exit(1);
+}
+
+static void
+run_reindex_command(PGconn *conn, ReindexType type, const char *name,
+ bool echo, bool verbose, bool concurrently, bool async)
+{
+ PQExpBufferData sql;
+ bool status;
+
+ Assert(name);
+
/* build the REINDEX query */
initPQExpBuffer(&sql);
@@ -344,7 +519,7 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
{
case REINDEX_DATABASE:
case REINDEX_SYSTEM:
- appendPQExpBufferStr(&sql, fmtId(PQdb(conn)));
+ appendPQExpBufferStr(&sql, fmtId(name));
break;
case REINDEX_INDEX:
case REINDEX_TABLE:
@@ -358,7 +533,17 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
/* finish the query */
appendPQExpBufferChar(&sql, ';');
- if (!executeMaintenanceCommand(conn, sql.data, echo))
+ if (async)
+ {
+ if (echo)
+ printf("%s\n", sql.data);
+
+ status = PQsendQuery(conn, sql.data) == 1;
+ }
+ else
+ status = executeMaintenanceCommand(conn, sql.data, echo);
+
+ if (!status)
{
switch (type)
{
@@ -383,20 +568,140 @@ reindex_one_database(const char *name, const char *dbname, ReindexType type,
name, PQdb(conn), PQerrorMessage(conn));
break;
}
- PQfinish(conn);
- exit(1);
+ if (!async)
+ {
+ PQfinish(conn);
+ exit(1);
+ }
}
- PQfinish(conn);
termPQExpBuffer(&sql);
}
+/*
+ * Prepare the list of objects to process by querying the catalogs.
+ *
+ * This function will return a SimpleStringList object containing the entire
+ * list of tables in the given database that should be processed by a parallel
+ * database-wide reindex (excluding system tables), or NULL if there's no such
+ * table.
+ */
+static SimpleStringList *
+get_parallel_object_list(PGconn *conn, ReindexType type,
+ SimpleStringList *user_list, bool echo)
+{
+ PQExpBufferData catalog_query;
+ PQExpBufferData buf;
+ PGresult *res;
+ SimpleStringList *tables;
+ int ntups,
+ i;
+
+ initPQExpBuffer(&catalog_query);
+
+ /*
+ * The queries here are using a safe search_path, so there's no need to
+ * fully qualify everything.
+ */
+ switch (type)
+ {
+ case REINDEX_DATABASE:
+ Assert(user_list == NULL);
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE ns.nspname != 'pg_catalog'\n"
+ " AND c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " ORDER BY c.relpages DESC;");
+ break;
+
+ case REINDEX_SCHEMA:
+ {
+ SimpleStringListCell *cell;
+ bool nsp_listed = false;
+
+ Assert(user_list != NULL);
+
+ /*
+ * All the tables from all the listed schemas are grabbed at once.
+ */
+ appendPQExpBuffer(&catalog_query,
+ "SELECT c.relname, ns.nspname\n"
+ " FROM pg_catalog.pg_class c\n"
+ " JOIN pg_catalog.pg_namespace ns"
+ " ON c.relnamespace = ns.oid\n"
+ " WHERE c.relkind IN ("
+ CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_MATVIEW) ")\n"
+ " AND ns.nspname IN (");
+
+ for (cell = user_list->head; cell; cell = cell->next)
+ {
+ const char *nspname = cell->val;
+
+ if (nsp_listed)
+ appendPQExpBuffer(&catalog_query, ", ");
+ else
+ nsp_listed = true;
+
+ appendStringLiteralConn(&catalog_query, nspname, conn);
+ }
+
+ appendPQExpBuffer(&catalog_query, ")\n"
+ " ORDER BY c.relpages DESC;");
+ }
+ break;
+
+ case REINDEX_SYSTEM:
+ case REINDEX_INDEX:
+ case REINDEX_TABLE:
+ Assert(false);
+ break;
+ }
+
+ res = executeQuery(conn, catalog_query.data, echo);
+ termPQExpBuffer(&catalog_query);
+
+ /*
+ * If no rows are returned, there are no matching tables, so we are done.
+ */
+ ntups = PQntuples(res);
+ if (ntups == 0)
+ {
+ PQclear(res);
+ PQfinish(conn);
+ return NULL;
+ }
+
+ tables = pg_malloc0(sizeof(SimpleStringList));
+
+ /* Build qualified identifiers for each table */
+ initPQExpBuffer(&buf);
+ for (i = 0; i < ntups; i++)
+ {
+ appendPQExpBufferStr(&buf,
+ fmtQualifiedId(PQgetvalue(res, i, 1),
+ PQgetvalue(res, i, 0)));
+
+ simple_string_list_append(tables, buf.data);
+ resetPQExpBuffer(&buf);
+ }
+ termPQExpBuffer(&buf);
+ PQclear(res);
+
+ return tables;
+}
+
static void
reindex_all_databases(const char *maintenance_db,
const char *host, const char *port,
const char *username, enum trivalue prompt_password,
const char *progname, bool echo, bool quiet, bool verbose,
- bool concurrently)
+ bool concurrently, int concurrentCons)
{
PGconn *conn;
PGresult *result;
@@ -423,9 +728,10 @@ reindex_all_databases(const char *maintenance_db,
appendPQExpBufferStr(&connstr, "dbname=");
appendConnStrVal(&connstr, dbname);
- reindex_one_database(NULL, connstr.data, REINDEX_DATABASE, host,
+ reindex_one_database(connstr.data, REINDEX_DATABASE, NULL, host,
port, username, prompt_password,
- progname, echo, verbose, concurrently);
+ progname, echo, verbose, concurrently,
+ concurrentCons);
}
termPQExpBuffer(&connstr);
@@ -444,6 +750,7 @@ help(const char *progname)
printf(_(" -d, --dbname=DBNAME database to reindex\n"));
printf(_(" -e, --echo show the commands being sent to the server\n"));
printf(_(" -i, --index=INDEX recreate specific index(es) only\n"));
+ printf(_(" -j, --jobs=NUM use this many concurrent connections to reindex\n"));
printf(_(" -q, --quiet don't write any messages\n"));
printf(_(" -s, --system reindex system catalogs\n"));
printf(_(" -S, --schema=SCHEMA reindex specific schema(s) only\n"));
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index 1af8ab70ad..bcd334eb8b 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -3,7 +3,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 34;
+use Test::More tests => 42;
program_help_ok('reindexdb');
program_version_ok('reindexdb');
@@ -77,3 +77,33 @@ $node->command_ok(
$node->command_ok(
[qw(reindexdb --echo --system dbname=template1)],
'reindexdb system with connection string');
+
+# parallel processing
+$node->safe_psql(
+ 'postgres', q|
+ CREATE SCHEMA s1;
+ CREATE TABLE s1.t1(id integer);
+ CREATE INDEX ON s1.t1(id);
+ CREATE SCHEMA s2;
+ CREATE TABLE s2.t2(id integer);
+ CREATE INDEX ON s2.t2(id);
+|);
+
+$node->command_fails(
+ [ 'reindexdb', '-j', '2', '-s', 'postgres' ],
+ 'reindexdb cannot process system catalogs in parallel');
+$node->issues_sql_like(
+ [ 'reindexdb', '-j', '2', 'postgres' ],
+ qr/statement:\ REINDEX SYSTEM postgres;
+.*statement:\ REINDEX TABLE public\.test1/s,
+ 'database reindexdb issues REINDEX SYSTEM first');
+$node->issues_sql_like(
+ [ 'reindexdb', '-j', '2', '-S', 's1', '-S', 's2', 'postgres' ],
+ qr/statement:\ REINDEX TABLE s1.t1;/,
+ 'schema-specific reindexdb with --jobs does a per-table REINDEX');
+$node->command_checks_all(
+ [ 'reindexdb', '-j', '2', '--concurrently', '-d', 'postgres' ],
+ 0,
+ [qr/^$/],
+ [qr/^reindexdb: warning: cannot reindex system catalogs concurrently, skipping all/s],
+ 'system reindexdb with --concurrently and --jobs skips catalogs');
--
2.22.0
0002-Forbid-index-with-jobs.patchtext/x-diff; charset=us-asciiDownload
From 5cd6f020d05f85b10426247b29a1b93e0e9ed1cf Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 26 Jul 2019 12:23:11 +0900
Subject: [PATCH 2/2] Forbid --index with --jobs
---
doc/src/sgml/ref/reindexdb.sgml | 6 ++----
src/bin/scripts/reindexdb.c | 21 ++++++++++++---------
src/bin/scripts/t/090_reindexdb.pl | 5 ++++-
3 files changed, 18 insertions(+), 14 deletions(-)
diff --git a/doc/src/sgml/ref/reindexdb.sgml b/doc/src/sgml/ref/reindexdb.sgml
index 477b77c6e9..5e21fbcc4e 100644
--- a/doc/src/sgml/ref/reindexdb.sgml
+++ b/doc/src/sgml/ref/reindexdb.sgml
@@ -183,10 +183,8 @@ PostgreSQL documentation
setting is high enough to accommodate all connections.
</para>
<para>
- Note that this option is ignored with the <option>--index</option>
- option to prevent deadlocks when processing multiple indexes from
- the same relation, and incompatible with the <option>--system</option>
- option.
+ Note that this option is incompatible with the <option>--index</option>
+ and <option>--system</option> options.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index a877689eec..ecd4490ecf 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -267,6 +267,17 @@ main(int argc, char *argv[])
}
else
{
+ /*
+ * Index-level REINDEX is not supported with multiple jobs as we
+ * cannot control the concurrent processing of multiple indexes
+ * depending on the same relation.
+ */
+ if (concurrentCons > 1 && indexes.head != NULL)
+ {
+ pg_log_error("cannot use multiple jobs to reindex indexes");
+ exit(1);
+ }
+
if (dbname == NULL)
{
if (getenv("PGDATABASE"))
@@ -283,17 +294,9 @@ main(int argc, char *argv[])
echo, verbose, concurrently, concurrentCons);
if (indexes.head != NULL)
- {
- /*
- * An index list cannot be processed by multiple connections, as
- * it could cause conflicts if reindexing multiple indexes from
- * the same table. We simply ignore the passed number of jobs if
- * any.
- */
reindex_one_database(dbname, REINDEX_INDEX, &indexes, host,
port, username, prompt_password, progname,
echo, verbose, concurrently, 1);
- }
if (tables.head != NULL)
reindex_one_database(dbname, REINDEX_TABLE, &tables, host,
@@ -409,12 +412,12 @@ reindex_one_database(const char *dbname, ReindexType type,
break;
case REINDEX_SYSTEM:
+ case REINDEX_INDEX:
/* not supported */
Assert(false);
break;
case REINDEX_TABLE:
- case REINDEX_INDEX:
/*
* Fall through. The list of items for indexes and tables is
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index bcd334eb8b..50144e9764 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -3,7 +3,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 42;
+use Test::More tests => 43;
program_help_ok('reindexdb');
program_version_ok('reindexdb');
@@ -92,6 +92,9 @@ $node->safe_psql(
$node->command_fails(
[ 'reindexdb', '-j', '2', '-s', 'postgres' ],
'reindexdb cannot process system catalogs in parallel');
+$node->command_fails(
+ [ 'reindexdb', '-j', '2', '-i', 'i1', 'postgres' ],
+ 'reindexdb cannot process indexes in parallel');
$node->issues_sql_like(
[ 'reindexdb', '-j', '2', 'postgres' ],
qr/statement:\ REINDEX SYSTEM postgres;
--
2.22.0
On Fri, Jul 26, 2019 at 5:27 AM Michael Paquier <michael@paquier.xyz> wrote:
On Thu, Jul 25, 2019 at 01:00:34PM +0200, Julien Rouhaud wrote:
The problem is that a user doing something like:
reindexdb -j48 -i some_index -S s1 -S s2 -S s3....
will probably be disappointed to learn that he has to run a specific
command for the index(es) that should be reindexed. Maybe we can
issue a warning that parallelism isn't used when an index list is
processed and user asked for multiple jobs?Arguments go in both directions as some other users may be surprised
by the performance of indexes as serialization is enforced.
Sure, but there is no easy solution in that case, as you'd have to do
all the work of spawning multiple reindexdb according to the
underlying table, so probably what will happen here is that there'll
just be two simple calls to reindexdb, one for the indexes, serialized
anyway, and one for everything else. My vote is still to allow it,
possibly emitting a notice or a warning.
I don't send a new patch since the --index wanted behavior is not
clear yet.So I am sending one patch (actually two) after a closer review that I
have spent time shaping into a committable state. And for this part I
have another suggestion that is to use a switch/case without a default
so as any newly-added object types would allow somebody to think about
those code paths as this would generate compiler warnings.
Thanks for that! I'm fine with using switch to avoid future bad surprises.
While reviewing I have found an extra bug in the logic: when using a
list of tables, the number of parallel slots is the minimum between
concurrentCons and tbl_count, but this does not get applied after
building a list of tables for a schema or database reindex, so we had
better recompute the number of items in reindex_one_database() before
allocating the number of parallel slots.
I see that you iterate over the SimpleStringList after it's generated.
Why not computing that while building it in get_parallel_object_list
(and keep the provided table list count) instead?
On Fri, Jul 26, 2019 at 09:36:32AM +0200, Julien Rouhaud wrote:
I see that you iterate over the SimpleStringList after it's generated.
Why not computing that while building it in get_parallel_object_list
(and keep the provided table list count) instead?
Yeah. I was hesitating to do that, or just break out of the counting
loop if there are more objects than concurrent jobs, but that's less
intuitive.
--
Michael
Hi
So here we go:
- 0001 is your original thing, with --jobs enforced to 1 for the index
part.
- 0002 is my addition to forbid --index with --jobs.I am fine to be outvoted regarding 0002, and it is the case based on
the state of this thread with 2:1. We could always revisit this
decision in this development cycle anyway.
Explicit is better than implicit, so I am +1 to commit both patches.
regards, Sergei
On Fri, Jul 26, 2019 at 10:53:03AM +0300, Sergei Kornilov wrote:
Explicit is better than implicit, so I am +1 to commit both patches.
Hence my count is incorrect:
- Forbid --jobs and --index: Michael P, Sergei K.
- Enforce --jobs=1 with --index: Julien R.
- Have no restrictions: 0.
--
Michael
On Fri, Jul 26, 2019 at 9:41 AM Michael Paquier <michael@paquier.xyz> wrote:
On Fri, Jul 26, 2019 at 09:36:32AM +0200, Julien Rouhaud wrote:
I see that you iterate over the SimpleStringList after it's generated.
Why not computing that while building it in get_parallel_object_list
(and keep the provided table list count) instead?Yeah. I was hesitating to do that, or just break out of the counting
loop if there are more objects than concurrent jobs, but that's less
intuitive.
That's probably still more intuitive than having the count coming from
either main() or from get_parallel_object_list() depending on the
process type, so I'm fine with that alternative. Maybe we could bite
the bullet and add a count meber to Simple*List, also providing a
macro to initialize a new list so that next time a field is added
there won't be a massive boilerplate code change?
On Sat, Jul 27, 2019 at 11:44:47AM +0200, Julien Rouhaud wrote:
That's probably still more intuitive than having the count coming from
either main() or from get_parallel_object_list() depending on the
process type, so I'm fine with that alternative. Maybe we could bite
the bullet and add a count meber to Simple*List, also providing a
macro to initialize a new list so that next time a field is added
there won't be a massive boilerplate code change?
Perhaps, we could discuss about that on a separate thread. For now I
have gone with the simplest approach of counting the items, and
stopping the count if there are more items than jobs. While reviewing
I have found a double-free in your patch when building a list of
relations for schemas or databases. If the list finishes empty,
PQfinish() was called twice on the connection, leading to a crash. I
have added a test for that, done an extra pass on the patch adjusting
a couple of things then committed the patch with the restriction on
--index and --jobs. This entry is now marked as committed in the CF
app.
--
Michael
On Sat, Jul 27, 2019 at 3:27 PM Michael Paquier <michael@paquier.xyz> wrote:
On Sat, Jul 27, 2019 at 11:44:47AM +0200, Julien Rouhaud wrote:
That's probably still more intuitive than having the count coming from
either main() or from get_parallel_object_list() depending on the
process type, so I'm fine with that alternative. Maybe we could bite
the bullet and add a count meber to Simple*List, also providing a
macro to initialize a new list so that next time a field is added
there won't be a massive boilerplate code change?Perhaps, we could discuss about that on a separate thread.
Agreed.
For now I
have gone with the simplest approach of counting the items, and
stopping the count if there are more items than jobs. While reviewing
I have found a double-free in your patch when building a list of
relations for schemas or databases. If the list finishes empty,
PQfinish() was called twice on the connection, leading to a crash. I
have added a test for that
Oops, thanks for spotting and fixing.
, done an extra pass on the patch adjusting
a couple of things then committed the patch with the restriction on
--index and --jobs. This entry is now marked as committed in the CF
app.
Thanks!
On Mon, Jul 22, 2019 at 01:05:32PM -0400, Alvaro Herrera wrote:
Fair enough. List has gotten quite sophisticated now, so I understand
the concern.
Just wondering something... List cells include one pointer, one
signed integer and an OID. The two last entries are basically 4-byte
each, hence could we reduce a bit the bloat by unifying both of them?
I understand that the distinction exists because both may not be of
the same size..
/me runs and hides
--
Michael
Michael Paquier <michael@paquier.xyz> writes:
Just wondering something... List cells include one pointer, one
signed integer and an OID. The two last entries are basically 4-byte
each, hence could we reduce a bit the bloat by unifying both of them?
We couldn't really simplify the API that way; for example,
lfirst_int and lfirst_oid *must* be different because they
must return different types. I think it'd be a bad idea
to have some parts of the API that distinguish the two types
while others pretend they're the same, so there's not much
room for shortening that.
You could imagine unifying the implementations of many of the
_int and _oid functions, but I can't get excited about that.
It would add confusion for not a lot of code savings.
I understand that the distinction exists because both may not be of
the same size..
Well, even more to the point, one's signed and one isn't.
In the long run, might we ever switch to 64-bit OIDs? I dunno.
Now that we kicked them out of user tables, it might be feasible,
but by the same token there's not much pressure to do it.
regards, tom lane
Hi,
On 2019-07-28 10:07:27 -0400, Tom Lane wrote:
In the long run, might we ever switch to 64-bit OIDs? I dunno.
Now that we kicked them out of user tables, it might be feasible,
but by the same token there's not much pressure to do it.
Depends on the the table, I'd say. Having toast tables have 64bit ids,
and not advance the oid counter, would be quite the advantage over the
current situation. Toasting performance craters once the oid counter has
wrapped. But obviously there are upgrade problems there - presumably
we'd need 'narrow" and 'wide' toast tables, or such.
Greetings,
Andres Freund
Andres Freund <andres@anarazel.de> writes:
On 2019-07-28 10:07:27 -0400, Tom Lane wrote:
In the long run, might we ever switch to 64-bit OIDs? I dunno.
Depends on the the table, I'd say. Having toast tables have 64bit ids,
and not advance the oid counter, would be quite the advantage over the
current situation. Toasting performance craters once the oid counter has
wrapped. But obviously there are upgrade problems there - presumably
we'd need 'narrow" and 'wide' toast tables, or such.
Yeah, but I'd be inclined to fix toast tables as a special case,
rather than widening OIDs in general. We could define the chunk
number as being int8 not OID for the "wide" style.
regards, tom lane