summaryrefslogtreecommitdiffstats
path: root/src/fe_utils/parallel_slot.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/fe_utils/parallel_slot.c')
-rw-r--r--src/fe_utils/parallel_slot.c563
1 files changed, 563 insertions, 0 deletions
diff --git a/src/fe_utils/parallel_slot.c b/src/fe_utils/parallel_slot.c
new file mode 100644
index 0000000..c65de7f
--- /dev/null
+++ b/src/fe_utils/parallel_slot.c
@@ -0,0 +1,563 @@
+/*-------------------------------------------------------------------------
+ *
+ * parallel_slot.c
+ * Parallel support for front-end parallel database connections
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/fe_utils/parallel_slot.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifdef WIN32
+#define FD_SETSIZE 1024 /* must set before winsock2.h is included */
+#endif
+
+#include "postgres_fe.h"
+
+#ifdef HAVE_SYS_SELECT_H
+#include <sys/select.h>
+#endif
+
+#include "common/logging.h"
+#include "fe_utils/cancel.h"
+#include "fe_utils/parallel_slot.h"
+#include "fe_utils/query_utils.h"
+
+#define ERRCODE_UNDEFINED_TABLE "42P01"
+
+static int select_loop(int maxFd, fd_set *workerset);
+static bool processQueryResult(ParallelSlot *slot, PGresult *result);
+
+/*
+ * Process (and delete) a query result. Returns true if there's no problem,
+ * false otherwise. It's up to the handler to decide what constitutes a
+ * problem.
+ */
+static bool
+processQueryResult(ParallelSlot *slot, PGresult *result)
+{
+ Assert(slot->handler != NULL);
+
+ /* On failure, the handler should return NULL after freeing the result */
+ if (!slot->handler(result, slot->connection, slot->handler_context))
+ return false;
+
+ /* Ok, we have to free it ourself */
+ PQclear(result);
+ return true;
+}
+
+/*
+ * 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 connection is busy.
+ */
+static bool
+consumeQueryResult(ParallelSlot *slot)
+{
+ bool ok = true;
+ PGresult *result;
+
+ SetCancelConn(slot->connection);
+ while ((result = PQgetResult(slot->connection)) != NULL)
+ {
+ if (!processQueryResult(slot, result))
+ ok = false;
+ }
+ ResetCancelConn();
+ return ok;
+}
+
+/*
+ * Wait until a file descriptor from the given set becomes readable.
+ *
+ * Returns the number of ready descriptors, or -1 on failure (including
+ * getting a cancel request).
+ */
+static int
+select_loop(int maxFd, fd_set *workerset)
+{
+ int i;
+ fd_set saveSet = *workerset;
+
+ if (CancelRequested)
+ return -1;
+
+ 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)
+ return -1; /* but not this */
+ if (i == 0)
+ continue; /* timeout (Win32 only) */
+ break;
+ }
+
+ return i;
+}
+
+/*
+ * Return the offset of a suitable idle slot, or -1 if none are available. If
+ * the given dbname is not null, only idle slots connected to the given
+ * database are considered suitable, otherwise all idle connected slots are
+ * considered suitable.
+ */
+static int
+find_matching_idle_slot(const ParallelSlotArray *sa, const char *dbname)
+{
+ int i;
+
+ for (i = 0; i < sa->numslots; i++)
+ {
+ if (sa->slots[i].inUse)
+ continue;
+
+ if (sa->slots[i].connection == NULL)
+ continue;
+
+ if (dbname == NULL ||
+ strcmp(PQdb(sa->slots[i].connection), dbname) == 0)
+ return i;
+ }
+ return -1;
+}
+
+/*
+ * Return the offset of the first slot without a database connection, or -1 if
+ * all slots are connected.
+ */
+static int
+find_unconnected_slot(const ParallelSlotArray *sa)
+{
+ int i;
+
+ for (i = 0; i < sa->numslots; i++)
+ {
+ if (sa->slots[i].inUse)
+ continue;
+
+ if (sa->slots[i].connection == NULL)
+ return i;
+ }
+
+ return -1;
+}
+
+/*
+ * Return the offset of the first idle slot, or -1 if all slots are busy.
+ */
+static int
+find_any_idle_slot(const ParallelSlotArray *sa)
+{
+ int i;
+
+ for (i = 0; i < sa->numslots; i++)
+ if (!sa->slots[i].inUse)
+ return i;
+
+ return -1;
+}
+
+/*
+ * Wait for any slot's connection to have query results, consume the results,
+ * and update the slot's status as appropriate. Returns true on success,
+ * false on cancellation, on error, or if no slots are connected.
+ */
+static bool
+wait_on_slots(ParallelSlotArray *sa)
+{
+ int i;
+ fd_set slotset;
+ int maxFd = 0;
+ PGconn *cancelconn = NULL;
+
+ /* We must reconstruct the fd_set for each call to select_loop */
+ FD_ZERO(&slotset);
+
+ for (i = 0; i < sa->numslots; i++)
+ {
+ int sock;
+
+ /* We shouldn't get here if we still have slots without connections */
+ Assert(sa->slots[i].connection != NULL);
+
+ sock = PQsocket(sa->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;
+
+ /* Keep track of the first valid connection we see. */
+ if (cancelconn == NULL)
+ cancelconn = sa->slots[i].connection;
+
+ FD_SET(sock, &slotset);
+ if (sock > maxFd)
+ maxFd = sock;
+ }
+
+ /*
+ * If we get this far with no valid connections, processing cannot
+ * continue.
+ */
+ if (cancelconn == NULL)
+ return false;
+
+ SetCancelConn(cancelconn);
+ i = select_loop(maxFd, &slotset);
+ ResetCancelConn();
+
+ /* failure? */
+ if (i < 0)
+ return false;
+
+ for (i = 0; i < sa->numslots; i++)
+ {
+ int sock;
+
+ sock = PQsocket(sa->slots[i].connection);
+
+ if (sock >= 0 && FD_ISSET(sock, &slotset))
+ {
+ /* select() says input is available, so consume it */
+ PQconsumeInput(sa->slots[i].connection);
+ }
+
+ /* Collect result(s) as long as any are available */
+ while (!PQisBusy(sa->slots[i].connection))
+ {
+ PGresult *result = PQgetResult(sa->slots[i].connection);
+
+ if (result != NULL)
+ {
+ /* Handle and discard the command result */
+ if (!processQueryResult(&sa->slots[i], result))
+ return false;
+ }
+ else
+ {
+ /* This connection has become idle */
+ sa->slots[i].inUse = false;
+ ParallelSlotClearHandler(&sa->slots[i]);
+ break;
+ }
+ }
+ }
+ return true;
+}
+
+/*
+ * Open a new database connection using the stored connection parameters and
+ * optionally a given dbname if not null, execute the stored initial command if
+ * any, and associate the new connection with the given slot.
+ */
+static void
+connect_slot(ParallelSlotArray *sa, int slotno, const char *dbname)
+{
+ const char *old_override;
+ ParallelSlot *slot = &sa->slots[slotno];
+
+ old_override = sa->cparams->override_dbname;
+ if (dbname)
+ sa->cparams->override_dbname = dbname;
+ slot->connection = connectDatabase(sa->cparams, sa->progname, sa->echo, false, true);
+ sa->cparams->override_dbname = old_override;
+
+ /*
+ * POSIX defines FD_SETSIZE as the highest file descriptor acceptable to
+ * FD_SET() and allied macros. Windows defines it as a ceiling on the
+ * count of file descriptors in the set, not a ceiling on the value of
+ * each file descriptor; see
+ * https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select
+ * and
+ * https://learn.microsoft.com/en-us/windows/win32/api/winsock/ns-winsock-fd_set.
+ * We can't ignore that, because Windows starts file descriptors at a
+ * higher value, delays reuse, and skips values. With less than ten
+ * concurrent file descriptors, opened and closed rapidly, one can reach
+ * file descriptor 1024.
+ *
+ * Doing a hard exit here is a bit grotty, but it doesn't seem worth
+ * complicating the API to make it less grotty.
+ */
+#ifdef WIN32
+ if (slotno >= FD_SETSIZE)
+ {
+ pg_log_error("too many jobs for this platform: %d", slotno);
+ exit(1);
+ }
+#else
+ {
+ int fd = PQsocket(slot->connection);
+
+ if (fd >= FD_SETSIZE)
+ {
+ pg_log_error("socket file descriptor out of range for select(): %d",
+ fd);
+ pg_log_error_hint("Try fewer jobs.");
+ exit(1);
+ }
+ }
+#endif
+
+ /* Setup the connection using the supplied command, if any. */
+ if (sa->initcmd)
+ executeCommand(slot->connection, sa->initcmd, sa->echo);
+}
+
+/*
+ * ParallelSlotsGetIdle
+ * Return a connection slot that is ready to execute a command.
+ *
+ * The slot returned is chosen as follows:
+ *
+ * If any idle slot already has an open connection, and if either dbname is
+ * null or the existing connection is to the given database, that slot will be
+ * returned allowing the connection to be reused.
+ *
+ * Otherwise, if any idle slot is not yet connected to any database, the slot
+ * will be returned with it's connection opened using the stored cparams and
+ * optionally the given dbname if not null.
+ *
+ * Otherwise, if any idle slot exists, an idle slot will be chosen and returned
+ * after having it's connection disconnected and reconnected using the stored
+ * cparams and optionally the given dbname if not null.
+ *
+ * Otherwise, if any slots have connections that are busy, 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. We then select a
+ * slot using the same rules as above.
+ *
+ * Otherwise, we cannot return a slot, which is an error, and NULL is returned.
+ *
+ * For any connection created, if the stored initcmd is not null, it will be
+ * executed as a command on the newly formed connection before the slot is
+ * returned.
+ *
+ * If an error occurs, NULL is returned.
+ */
+ParallelSlot *
+ParallelSlotsGetIdle(ParallelSlotArray *sa, const char *dbname)
+{
+ int offset;
+
+ Assert(sa);
+ Assert(sa->numslots > 0);
+
+ while (1)
+ {
+ /* First choice: a slot already connected to the desired database. */
+ offset = find_matching_idle_slot(sa, dbname);
+ if (offset >= 0)
+ {
+ sa->slots[offset].inUse = true;
+ return &sa->slots[offset];
+ }
+
+ /* Second choice: a slot not connected to any database. */
+ offset = find_unconnected_slot(sa);
+ if (offset >= 0)
+ {
+ connect_slot(sa, offset, dbname);
+ sa->slots[offset].inUse = true;
+ return &sa->slots[offset];
+ }
+
+ /* Third choice: a slot connected to the wrong database. */
+ offset = find_any_idle_slot(sa);
+ if (offset >= 0)
+ {
+ disconnectDatabase(sa->slots[offset].connection);
+ sa->slots[offset].connection = NULL;
+ connect_slot(sa, offset, dbname);
+ sa->slots[offset].inUse = true;
+ return &sa->slots[offset];
+ }
+
+ /*
+ * Fourth choice: block until one or more slots become available. If
+ * any slots hit a fatal error, we'll find out about that here and
+ * return NULL.
+ */
+ if (!wait_on_slots(sa))
+ return NULL;
+ }
+}
+
+/*
+ * ParallelSlotsSetup
+ * Prepare a set of parallel slots but do not connect to any database.
+ *
+ * This creates and initializes a set of slots, marking all parallel slots as
+ * free and ready to use. Establishing connections is delayed until requesting
+ * a free slot. The cparams, progname, echo, and initcmd are stored for later
+ * use and must remain valid for the lifetime of the returned array.
+ */
+ParallelSlotArray *
+ParallelSlotsSetup(int numslots, ConnParams *cparams, const char *progname,
+ bool echo, const char *initcmd)
+{
+ ParallelSlotArray *sa;
+
+ Assert(numslots > 0);
+ Assert(cparams != NULL);
+ Assert(progname != NULL);
+
+ sa = (ParallelSlotArray *) palloc0(offsetof(ParallelSlotArray, slots) +
+ numslots * sizeof(ParallelSlot));
+
+ sa->numslots = numslots;
+ sa->cparams = cparams;
+ sa->progname = progname;
+ sa->echo = echo;
+ sa->initcmd = initcmd;
+
+ return sa;
+}
+
+/*
+ * ParallelSlotsAdoptConn
+ * Assign an open connection to the slots array for reuse.
+ *
+ * This turns over ownership of an open connection to a slots array. The
+ * caller should not further use or close the connection. All the connection's
+ * parameters (user, host, port, etc.) except possibly dbname should match
+ * those of the slots array's cparams, as given in ParallelSlotsSetup. If
+ * these parameters differ, subsequent behavior is undefined.
+ */
+void
+ParallelSlotsAdoptConn(ParallelSlotArray *sa, PGconn *conn)
+{
+ int offset;
+
+ offset = find_unconnected_slot(sa);
+ if (offset >= 0)
+ sa->slots[offset].connection = conn;
+ else
+ disconnectDatabase(conn);
+}
+
+/*
+ * ParallelSlotsTerminate
+ * Clean up a set of parallel slots
+ *
+ * Iterate through all connections in a given set of ParallelSlots and
+ * terminate all connections.
+ */
+void
+ParallelSlotsTerminate(ParallelSlotArray *sa)
+{
+ int i;
+
+ for (i = 0; i < sa->numslots; i++)
+ {
+ PGconn *conn = sa->slots[i].connection;
+
+ if (conn == NULL)
+ continue;
+
+ disconnectDatabase(conn);
+ }
+}
+
+/*
+ * ParallelSlotsWaitCompletion
+ *
+ * Wait for all connections to finish, returning false if at least one
+ * error has been found on the way.
+ */
+bool
+ParallelSlotsWaitCompletion(ParallelSlotArray *sa)
+{
+ int i;
+
+ for (i = 0; i < sa->numslots; i++)
+ {
+ if (sa->slots[i].connection == NULL)
+ continue;
+ if (!consumeQueryResult(&sa->slots[i]))
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * TableCommandResultHandler
+ *
+ * ParallelSlotResultHandler for results of commands (not queries) against
+ * tables.
+ *
+ * Requires that the result status is either PGRES_COMMAND_OK or an error about
+ * a missing table. This is useful for utilities that compile a list of tables
+ * to process and then run commands (vacuum, reindex, or whatever) against
+ * those tables, as there is a race condition between the time the list is
+ * compiled and the time the command attempts to open the table.
+ *
+ * For missing tables, logs an error but allows processing to continue.
+ *
+ * For all other errors, logs an error and terminates further processing.
+ *
+ * res: PGresult from the query executed on the slot's connection
+ * conn: connection belonging to the slot
+ * context: unused
+ */
+bool
+TableCommandResultHandler(PGresult *res, PGconn *conn, void *context)
+{
+ Assert(res != NULL);
+ Assert(conn != NULL);
+
+ /*
+ * 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(res) != PGRES_COMMAND_OK)
+ {
+ char *sqlState = PQresultErrorField(res, 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(res);
+ return false;
+ }
+ }
+
+ return true;
+}