public inbox for [email protected]  
help / color / mirror / Atom feed
From: Yurii Rashkovskii <[email protected]>
To: [email protected]
Subject: [PATCH] Allow Postgres to pick an unused port to listen
Date: Wed, 29 Mar 2023 12:18:55 +0700
Message-ID: <CA+RLCQwJBGAnBo7OZ6QxqJjEn6w=Mju9GrHQmwFxX_io+4Cwhw@mail.gmail.com> (raw)

Hi,

I would like to suggest a patch against master (although it may be worth
backporting it) that makes it possible to listen on any unused port.

The main motivation is running colocated instances of Postgres (such as
test benches) without having to coordinate port allocation in an
unnecessarily complicated way.

Instead, with this patch, one can specify `port` as `0` (the "wildcard"
port) and retrieve the assigned port from postmaster.pid

I believe there is no significant performance or another impact as it is a
tiny bit of conditional functionality executed during startup.

The patch builds and `make check` succeeds. The patch does not add a test;
however, I am trying to figure out if this behaviour can be tested
automatically.


-- 
http://omnigres.org
Y.


Attachments:

  [application/octet-stream] V1-0001-Allow-listening-port-to-be-0.patch (7.8K, ../CA+RLCQwJBGAnBo7OZ6QxqJjEn6w=Mju9GrHQmwFxX_io+4Cwhw@mail.gmail.com/3-V1-0001-Allow-listening-port-to-be-0.patch)
  download | inline diff:
From 82230d3f76711dd236cd3e50b6baf078552807b0 Mon Sep 17 00:00:00 2001
From: Yurii Rashkovskii <[email protected]>
Date: Wed, 29 Mar 2023 11:54:50 +0700
Subject: [PATCH] Allow listening port to be 0

This makes it possible to start postgres on an unused port instead of
trying to reliably pick it in cases where this is desirable, such as
test benches or colocated database instances.

This is done by retrieving the assigned port number using `getsockname`.

Those using this feature can retrieve the assigned port through
postmaster.pid
---
 doc/src/sgml/config.sgml            |  4 ++
 src/backend/libpq/pqcomm.c          | 77 +++++++++++++++++++++++++++--
 src/backend/postmaster/postmaster.c |  8 +--
 src/backend/utils/misc/guc_tables.c |  2 +-
 src/include/libpq/libpq.h           |  3 ++
 5 files changed, 84 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bda0da2dc8..84be4e33c7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,10 @@ include_dir 'conf.d'
         same port number is used for all IP addresses the server listens on.
         This parameter can only be set at server start.
        </para>
+       <para>
+        The port can be set to 0 to make Postgres pick an unused port number.
+        The assigned port number can be then retrieved from <entry><filename>postmaster.pid</filename></entry>.
+       </para>
       </listitem>
      </varlistentry>
 
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index da5bb5fc5d..f713437ce4 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -302,11 +302,12 @@ socket_close(int code, Datum arg)
  *		Stream functions are used for vanilla TCP connection protocol.
  */
 
-
 /*
- * StreamServerPort -- open a "listening" port to accept connections.
+ * StreamServerPortReturn -- open a "listening" port to accept connections.
+ *
+ * family should be AF_UNIX or AF_UNSPEC; portNumberPtr is a non-NULL pointer
+ * to a port number.
  *
- * family should be AF_UNIX or AF_UNSPEC; portNumber is the port number.
  * For AF_UNIX ports, hostName should be NULL and unixSocketDir must be
  * specified.  For TCP ports, hostName is either NULL for all interfaces or
  * the interface to listen on, and unixSocketDir is ignored (can be NULL).
@@ -314,13 +315,20 @@ socket_close(int code, Datum arg)
  * Successfully opened sockets are added to the ListenSocket[] array (of
  * length MaxListen), at the first position that isn't PGINVALID_SOCKET.
  *
+ * This function allows retrieving assigned port number through portNumberPtr
+ * (if the original value of *portNumberPtr is 0, signifying an unused port
+ * selection)
+ *
+ * If the retrieval of the assigned port number is not necessary, StreamServerPort
+ * can be used instead.
+ *
  * RETURNS: STATUS_OK or STATUS_ERROR
  */
-
 int
-StreamServerPort(int family, const char *hostName, unsigned short portNumber,
+StreamServerPortReturn(int family, const char *hostName, unsigned short *portNumberPtr,
 				 const char *unixSocketDir,
 				 pgsocket ListenSocket[], int MaxListen)
+
 {
 	pgsocket	fd;
 	int			err;
@@ -342,6 +350,11 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
 	int			one = 1;
 #endif
 
+	/* portNumberPtr must contain a value */
+	Assert(portNumberPtr != NULL);
+	unsigned short portNumber = *portNumberPtr;
+
+
 	/* Initialize hint structure */
 	MemSet(&hint, 0, sizeof(hint));
 	hint.ai_family = family;
@@ -539,6 +552,39 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
 				closesocket(fd);
 				break;
 			}
+		} else if (portNumber == 0) 
+		{
+			/* Return actual, effective portNumber through portNumberPtr.
+			 *
+			 * The reason it works uniformly across different socket types is because
+			 * port number is updated in a sequence and on the following iterations, it will
+			 * be no longer set to 0. Therefore, all sockets will share the same IP address.
+			 *
+			 * If it is impossible to listen on a socket on the port that was unused in one family,
+			 * it will result in an error.
+			 */
+			struct sockaddr_in sockaddr;
+			StaticAssertStmt(offsetof(struct sockaddr_in, sin_port) == offsetof(struct sockaddr_in6, sin6_port),
+					"sockaddr_in and sockaddr_in6 must have port at the same offset");
+			socklen_t socksize = sizeof(sockaddr);
+
+			if (getsockname(fd, (struct sockaddr *)&sockaddr, &socksize) == -1)
+			{
+				int	saved_errno = errno;
+				ereport(LOG, errmsg("getsockname failed with: %s", strerror(saved_errno)));
+				closesocket(fd);
+				break;
+			}
+
+			if (addr->ai_family == AF_INET)
+			{
+				*portNumberPtr = ntohs(sockaddr.sin_port);
+			} else if (addr->ai_family == AF_INET6)
+			{
+				*portNumberPtr = ntohs(((struct sockaddr_in6 *)&sockaddr)->sin6_port);
+			}
+
+			portNumber = *portNumberPtr;
 		}
 
 		/*
@@ -582,6 +628,27 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
 	return STATUS_OK;
 }
 
+/*
+ * StreamServerPort -- open a "listening" port to accept connections.
+ *
+ * family should be AF_UNIX or AF_UNSPEC; portNumber is the port number.
+ * For AF_UNIX ports, hostName should be NULL and unixSocketDir must be
+ * specified.  For TCP ports, hostName is either NULL for all interfaces or
+ * the interface to listen on, and unixSocketDir is ignored (can be NULL).
+ *
+ * Successfully opened sockets are added to the ListenSocket[] array (of
+ * length MaxListen), at the first position that isn't PGINVALID_SOCKET.
+ *
+ * RETURNS: STATUS_OK or STATUS_ERROR
+ */
+
+int
+StreamServerPort(int family, const char *hostName, unsigned short portNumber,
+				 const char *unixSocketDir,
+				 pgsocket ListenSocket[], int MaxListen)
+{
+	return StreamServerPortReturn(family, hostName, &portNumber, unixSocketDir, ListenSocket, MaxListen);
+}
 
 /*
  * Lock_AF_UNIX -- configure unix socket file path
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 4c49393fc5..1e4029783c 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -1211,13 +1211,13 @@ PostmasterMain(int argc, char *argv[])
 			char	   *curhost = (char *) lfirst(l);
 
 			if (strcmp(curhost, "*") == 0)
-				status = StreamServerPort(AF_UNSPEC, NULL,
-										  (unsigned short) PostPortNumber,
+				status = StreamServerPortReturn(AF_UNSPEC, NULL,
+										  (unsigned short *) &PostPortNumber,
 										  NULL,
 										  ListenSocket, MAXLISTEN);
 			else
-				status = StreamServerPort(AF_UNSPEC, curhost,
-										  (unsigned short) PostPortNumber,
+				status = StreamServerPortReturn(AF_UNSPEC, curhost,
+										  (unsigned short *) &PostPortNumber,
 										  NULL,
 										  ListenSocket, MAXLISTEN);
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8062589efd..11b7c3d46f 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2263,7 +2263,7 @@ struct config_int ConfigureNamesInt[] =
 			NULL
 		},
 		&PostPortNumber,
-		DEF_PGPORT, 1, 65535,
+		DEF_PGPORT, 0, 65535,
 		NULL, NULL, NULL
 	},
 
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 50fc781f47..c875da55e2 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -67,6 +67,9 @@ extern PGDLLIMPORT WaitEventSet *FeBeWaitSet;
 extern int	StreamServerPort(int family, const char *hostName,
 							 unsigned short portNumber, const char *unixSocketDir,
 							 pgsocket ListenSocket[], int MaxListen);
+extern int	StreamServerPortReturn(int family, const char *hostName,
+							 unsigned short *portNumberPtr, const char *unixSocketDir,
+							 pgsocket ListenSocket[], int MaxListen);
 extern int	StreamConnection(pgsocket server_fd, Port *port);
 extern void StreamClose(pgsocket sock);
 extern void TouchSocketFiles(void);
-- 
2.37.1 (Apple Git-137.1)



view thread (2+ messages)

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected]
  Subject: Re: [PATCH] Allow Postgres to pick an unused port to listen
  In-Reply-To: <CA+RLCQwJBGAnBo7OZ6QxqJjEn6w=Mju9GrHQmwFxX_io+4Cwhw@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox