1
0
mirror of https://github.com/postgres/postgres.git synced 2025-09-02 04:21:28 +03:00

libpq: Add encrypted and non-blocking query cancellation routines

The existing PQcancel API uses blocking IO, which makes PQcancel
impossible to use in an event loop based codebase without blocking the
event loop until the call returns.  It also doesn't encrypt the
connection over which the cancel request is sent, even when the original
connection required encryption.

This commit adds a PQcancelConn struct and assorted functions, which
provide a better mechanism of sending cancel requests; in particular all
the encryption used in the original connection are also used in the
cancel connection.  The main entry points are:

- PQcancelCreate creates the PQcancelConn based on the original
  connection (but does not establish an actual connection).
- PQcancelStart can be used to initiate non-blocking cancel requests,
  using encryption if the original connection did so, which must be
  pumped using
- PQcancelPoll.
- PQcancelReset puts a PQcancelConn back in state so that it can be
  reused to send a new cancel request to the same connection.
- PQcancelBlocking is a simpler-to-use blocking API that still uses
  encryption.

Additional functions are
 - PQcancelStatus, mimicks PQstatus;
 - PQcancelSocket, mimicks PQcancelSocket;
 - PQcancelErrorMessage, mimicks PQerrorMessage;
 - PQcancelFinish, mimicks PQfinish.

Author: Jelte Fennema-Nio <postgres@jeltef.nl>
Reviewed-by: Denis Laxalde <denis.laxalde@dalibo.com>
Discussion: https://postgr.es/m/AM5PR83MB0178D3B31CA1B6EC4A8ECC42F7529@AM5PR83MB0178.EURPRD83.prod.outlook.com
This commit is contained in:
Alvaro Herrera
2024-03-12 17:32:25 +01:00
parent cb9663e20d
commit 61461a300c
8 changed files with 1044 additions and 55 deletions

View File

@@ -616,8 +616,17 @@ pqDropServerData(PGconn *conn)
conn->write_failed = false;
free(conn->write_err_msg);
conn->write_err_msg = NULL;
conn->be_pid = 0;
conn->be_key = 0;
/*
* Cancel connections need to retain their be_pid and be_key across
* PQcancelReset invocations, otherwise they would not have access to the
* secret token of the connection they are supposed to cancel.
*/
if (!conn->cancelRequest)
{
conn->be_pid = 0;
conn->be_key = 0;
}
}
@@ -923,6 +932,45 @@ fillPGconn(PGconn *conn, PQconninfoOption *connOptions)
return true;
}
/*
* Copy over option values from srcConn to dstConn
*
* Don't put anything cute here --- intelligence should be in
* connectOptions2 ...
*
* Returns true on success. On failure, returns false and sets error message of
* dstConn.
*/
bool
pqCopyPGconn(PGconn *srcConn, PGconn *dstConn)
{
const internalPQconninfoOption *option;
/* copy over connection options */
for (option = PQconninfoOptions; option->keyword; option++)
{
if (option->connofs >= 0)
{
const char **tmp = (const char **) ((char *) srcConn + option->connofs);
if (*tmp)
{
char **dstConnmember = (char **) ((char *) dstConn + option->connofs);
if (*dstConnmember)
free(*dstConnmember);
*dstConnmember = strdup(*tmp);
if (*dstConnmember == NULL)
{
libpq_append_conn_error(dstConn, "out of memory");
return false;
}
}
}
}
return true;
}
/*
* connectOptions1
*
@@ -2308,10 +2356,18 @@ pqConnectDBStart(PGconn *conn)
* Set up to try to connect to the first host. (Setting whichhost = -1 is
* a bit of a cheat, but PQconnectPoll will advance it to 0 before
* anything else looks at it.)
*
* Cancel requests are special though, they should only try one host and
* address, and these fields have already been set up in PQcancelCreate,
* so leave these fields alone for cancel requests.
*/
conn->whichhost = -1;
conn->try_next_addr = false;
conn->try_next_host = true;
if (!conn->cancelRequest)
{
conn->whichhost = -1;
conn->try_next_host = true;
conn->try_next_addr = false;
}
conn->status = CONNECTION_NEEDED;
/* Also reset the target_server_type state if needed */
@@ -2453,7 +2509,10 @@ pqConnectDBComplete(PGconn *conn)
/*
* Now try to advance the state machine.
*/
flag = PQconnectPoll(conn);
if (conn->cancelRequest)
flag = PQcancelPoll((PGcancelConn *) conn);
else
flag = PQconnectPoll(conn);
}
}
@@ -2578,13 +2637,17 @@ keep_going: /* We will come back to here until there is
* Oops, no more hosts.
*
* If we are trying to connect in "prefer-standby" mode, then drop
* the standby requirement and start over.
* the standby requirement and start over. Don't do this for
* cancel requests though, since we are certain the list of
* servers won't change as the target_server_type option is not
* applicable to those connections.
*
* Otherwise, an appropriate error message is already set up, so
* we just need to set the right status.
*/
if (conn->target_server_type == SERVER_TYPE_PREFER_STANDBY &&
conn->nconnhost > 0)
conn->nconnhost > 0 &&
!conn->cancelRequest)
{
conn->target_server_type = SERVER_TYPE_PREFER_STANDBY_PASS2;
conn->whichhost = 0;
@@ -3226,6 +3289,29 @@ keep_going: /* We will come back to here until there is
}
#endif /* USE_SSL */
/*
* For cancel requests this is as far as we need to go in the
* connection establishment. Now we can actually send our
* cancellation request.
*/
if (conn->cancelRequest)
{
CancelRequestPacket cancelpacket;
packetlen = sizeof(cancelpacket);
cancelpacket.cancelRequestCode = (MsgType) pg_hton32(CANCEL_REQUEST_CODE);
cancelpacket.backendPID = pg_hton32(conn->be_pid);
cancelpacket.cancelAuthCode = pg_hton32(conn->be_key);
if (pqPacketSend(conn, 0, &cancelpacket, packetlen) != STATUS_OK)
{
libpq_append_conn_error(conn, "could not send cancel packet: %s",
SOCK_STRERROR(SOCK_ERRNO, sebuf, sizeof(sebuf)));
goto error_return;
}
conn->status = CONNECTION_AWAITING_RESPONSE;
return PGRES_POLLING_READING;
}
/*
* Build the startup packet.
*/
@@ -3975,8 +4061,14 @@ keep_going: /* We will come back to here until there is
}
}
/* We can release the address list now. */
release_conn_addrinfo(conn);
/*
* For non cancel requests we can release the address list
* now. For cancel requests we never actually resolve
* addresses and instead the addrinfo exists for the lifetime
* of the connection.
*/
if (!conn->cancelRequest)
release_conn_addrinfo(conn);
/*
* Contents of conn->errorMessage are no longer interesting
@@ -4344,6 +4436,7 @@ freePGconn(PGconn *conn)
free(conn->events[i].name);
}
release_conn_addrinfo(conn);
pqReleaseConnHosts(conn);
free(conn->client_encoding_initial);
@@ -4495,6 +4588,13 @@ release_conn_addrinfo(PGconn *conn)
static void
sendTerminateConn(PGconn *conn)
{
/*
* The Postgres cancellation protocol does not have a notion of a
* Terminate message, so don't send one.
*/
if (conn->cancelRequest)
return;
/*
* Note that the protocol doesn't allow us to send Terminate messages
* during the startup phase.
@@ -4548,7 +4648,14 @@ pqClosePGconn(PGconn *conn)
conn->pipelineStatus = PQ_PIPELINE_OFF;
pqClearAsyncResult(conn); /* deallocate result */
pqClearConnErrorState(conn);
release_conn_addrinfo(conn);
/*
* Release addrinfo, but since cancel requests never change their addrinfo
* we don't do that. Otherwise we would have to rebuild it during a
* PQcancelReset.
*/
if (!conn->cancelRequest)
release_conn_addrinfo(conn);
/* Reset all state obtained from server, too */
pqDropServerData(conn);