Skip to content

Commit 61461a3

Browse files
committed
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
1 parent cb9663e commit 61461a3

File tree

8 files changed

+1044
-55
lines changed

8 files changed

+1044
-55
lines changed

doc/src/sgml/libpq.sgml

+467-40
Large diffs are not rendered by default.

src/interfaces/libpq/exports.txt

+9
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,12 @@ PQsendClosePrepared 190
193193
PQsendClosePortal 191
194194
PQchangePassword 192
195195
PQsendPipelineSync 193
196+
PQcancelBlocking 194
197+
PQcancelStart 195
198+
PQcancelCreate 196
199+
PQcancelPoll 197
200+
PQcancelStatus 198
201+
PQcancelSocket 199
202+
PQcancelErrorMessage 200
203+
PQcancelReset 201
204+
PQcancelFinish 202

src/interfaces/libpq/fe-cancel.c

+295-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@
2222
#include "port/pg_bswap.h"
2323

2424

25+
/*
26+
* pg_cancel_conn (backing struct for PGcancelConn) is a wrapper around a
27+
* PGconn to send cancellations using PQcancelBlocking and PQcancelStart.
28+
* This isn't just a typedef because we want the compiler to complain when a
29+
* PGconn is passed to a function that expects a PGcancelConn, and vice versa.
30+
*/
31+
struct pg_cancel_conn
32+
{
33+
PGconn conn;
34+
};
35+
2536
/*
2637
* pg_cancel (backing struct for PGcancel) stores all data necessary to send a
2738
* cancel request.
@@ -41,6 +52,289 @@ struct pg_cancel
4152
};
4253

4354

55+
/*
56+
* PQcancelCreate
57+
*
58+
* Create and return a PGcancelConn, which can be used to securely cancel a
59+
* query on the given connection.
60+
*
61+
* This requires either following the non-blocking flow through
62+
* PQcancelStart() and PQcancelPoll(), or the blocking PQcancelBlocking().
63+
*/
64+
PGcancelConn *
65+
PQcancelCreate(PGconn *conn)
66+
{
67+
PGconn *cancelConn = pqMakeEmptyPGconn();
68+
pg_conn_host originalHost;
69+
70+
if (cancelConn == NULL)
71+
return NULL;
72+
73+
/* Check we have an open connection */
74+
if (!conn)
75+
{
76+
libpq_append_conn_error(cancelConn, "passed connection was NULL");
77+
return (PGcancelConn *) cancelConn;
78+
}
79+
80+
if (conn->sock == PGINVALID_SOCKET)
81+
{
82+
libpq_append_conn_error(cancelConn, "passed connection is not open");
83+
return (PGcancelConn *) cancelConn;
84+
}
85+
86+
/*
87+
* Indicate that this connection is used to send a cancellation
88+
*/
89+
cancelConn->cancelRequest = true;
90+
91+
if (!pqCopyPGconn(conn, cancelConn))
92+
return (PGcancelConn *) cancelConn;
93+
94+
/*
95+
* Compute derived options
96+
*/
97+
if (!pqConnectOptions2(cancelConn))
98+
return (PGcancelConn *) cancelConn;
99+
100+
/*
101+
* Copy cancellation token data from the original connnection
102+
*/
103+
cancelConn->be_pid = conn->be_pid;
104+
cancelConn->be_key = conn->be_key;
105+
106+
/*
107+
* Cancel requests should not iterate over all possible hosts. The request
108+
* needs to be sent to the exact host and address that the original
109+
* connection used. So we manually create the host and address arrays with
110+
* a single element after freeing the host array that we generated from
111+
* the connection options.
112+
*/
113+
pqReleaseConnHosts(cancelConn);
114+
cancelConn->nconnhost = 1;
115+
cancelConn->naddr = 1;
116+
117+
cancelConn->connhost = calloc(cancelConn->nconnhost, sizeof(pg_conn_host));
118+
if (!cancelConn->connhost)
119+
goto oom_error;
120+
121+
originalHost = conn->connhost[conn->whichhost];
122+
if (originalHost.host)
123+
{
124+
cancelConn->connhost[0].host = strdup(originalHost.host);
125+
if (!cancelConn->connhost[0].host)
126+
goto oom_error;
127+
}
128+
if (originalHost.hostaddr)
129+
{
130+
cancelConn->connhost[0].hostaddr = strdup(originalHost.hostaddr);
131+
if (!cancelConn->connhost[0].hostaddr)
132+
goto oom_error;
133+
}
134+
if (originalHost.port)
135+
{
136+
cancelConn->connhost[0].port = strdup(originalHost.port);
137+
if (!cancelConn->connhost[0].port)
138+
goto oom_error;
139+
}
140+
if (originalHost.password)
141+
{
142+
cancelConn->connhost[0].password = strdup(originalHost.password);
143+
if (!cancelConn->connhost[0].password)
144+
goto oom_error;
145+
}
146+
147+
cancelConn->addr = calloc(cancelConn->naddr, sizeof(AddrInfo));
148+
if (!cancelConn->connhost)
149+
goto oom_error;
150+
151+
cancelConn->addr[0].addr = conn->raddr;
152+
cancelConn->addr[0].family = conn->raddr.addr.ss_family;
153+
154+
cancelConn->status = CONNECTION_ALLOCATED;
155+
return (PGcancelConn *) cancelConn;
156+
157+
oom_error:
158+
conn->status = CONNECTION_BAD;
159+
libpq_append_conn_error(cancelConn, "out of memory");
160+
return (PGcancelConn *) cancelConn;
161+
}
162+
163+
164+
/*
165+
* PQcancelBlocking
166+
*
167+
* Send a cancellation request in a blocking fashion.
168+
* Returns 1 if successful 0 if not.
169+
*/
170+
int
171+
PQcancelBlocking(PGcancelConn *cancelConn)
172+
{
173+
if (!PQcancelStart(cancelConn))
174+
return 0;
175+
return pqConnectDBComplete(&cancelConn->conn);
176+
}
177+
178+
/*
179+
* PQcancelStart
180+
*
181+
* Starts sending a cancellation request in a non-blocking fashion. Returns
182+
* 1 if successful 0 if not.
183+
*/
184+
int
185+
PQcancelStart(PGcancelConn *cancelConn)
186+
{
187+
if (!cancelConn || cancelConn->conn.status == CONNECTION_BAD)
188+
return 0;
189+
190+
if (cancelConn->conn.status != CONNECTION_ALLOCATED)
191+
{
192+
libpq_append_conn_error(&cancelConn->conn,
193+
"cancel request is already being sent on this connection");
194+
cancelConn->conn.status = CONNECTION_BAD;
195+
return 0;
196+
}
197+
198+
return pqConnectDBStart(&cancelConn->conn);
199+
}
200+
201+
/*
202+
* PQcancelPoll
203+
*
204+
* Poll a cancel connection. For usage details see PQconnectPoll.
205+
*/
206+
PostgresPollingStatusType
207+
PQcancelPoll(PGcancelConn *cancelConn)
208+
{
209+
PGconn *conn = &cancelConn->conn;
210+
int n;
211+
212+
/*
213+
* We leave most of the connection establishement to PQconnectPoll, since
214+
* it's very similar to normal connection establishment. But once we get
215+
* to the CONNECTION_AWAITING_RESPONSE we need to start doing our own
216+
* thing.
217+
*/
218+
if (conn->status != CONNECTION_AWAITING_RESPONSE)
219+
{
220+
return PQconnectPoll(conn);
221+
}
222+
223+
/*
224+
* At this point we are waiting on the server to close the connection,
225+
* which is its way of communicating that the cancel has been handled.
226+
*/
227+
228+
n = pqReadData(conn);
229+
230+
if (n == 0)
231+
return PGRES_POLLING_READING;
232+
233+
#ifndef WIN32
234+
235+
/*
236+
* If we receive an error report it, but only if errno is non-zero.
237+
* Otherwise we assume it's an EOF, which is what we expect from the
238+
* server.
239+
*
240+
* We skip this for Windows, because Windows is a bit special in its EOF
241+
* behaviour for TCP. Sometimes it will error with an ECONNRESET when
242+
* there is a clean connection closure. See these threads for details:
243+
* https://www.postgresql.org/message-id/flat/90b34057-4176-7bb0-0dbb-9822a5f6425b%40greiz-reinsdorf.de
244+
*
245+
* https://www.postgresql.org/message-id/flat/CA%2BhUKG%2BOeoETZQ%3DQw5Ub5h3tmwQhBmDA%3DnuNO3KG%3DzWfUypFAw%40mail.gmail.com
246+
*
247+
* PQcancel ignores such errors and reports success for the cancellation
248+
* anyway, so even if this is not always correct we do the same here.
249+
*/
250+
if (n < 0 && errno != 0)
251+
{
252+
conn->status = CONNECTION_BAD;
253+
return PGRES_POLLING_FAILED;
254+
}
255+
#endif
256+
257+
/*
258+
* We don't expect any data, only connection closure. So if we strangely
259+
* do receive some data we consider that an error.
260+
*/
261+
if (n > 0)
262+
{
263+
libpq_append_conn_error(conn, "received unexpected response from server");
264+
conn->status = CONNECTION_BAD;
265+
return PGRES_POLLING_FAILED;
266+
}
267+
268+
/*
269+
* Getting here means that we received an EOF, which is what we were
270+
* expecting -- the cancel request has completed.
271+
*/
272+
cancelConn->conn.status = CONNECTION_OK;
273+
resetPQExpBuffer(&conn->errorMessage);
274+
return PGRES_POLLING_OK;
275+
}
276+
277+
/*
278+
* PQcancelStatus
279+
*
280+
* Get the status of a cancel connection.
281+
*/
282+
ConnStatusType
283+
PQcancelStatus(const PGcancelConn *cancelConn)
284+
{
285+
return PQstatus(&cancelConn->conn);
286+
}
287+
288+
/*
289+
* PQcancelSocket
290+
*
291+
* Get the socket of the cancel connection.
292+
*/
293+
int
294+
PQcancelSocket(const PGcancelConn *cancelConn)
295+
{
296+
return PQsocket(&cancelConn->conn);
297+
}
298+
299+
/*
300+
* PQcancelErrorMessage
301+
*
302+
* Get the socket of the cancel connection.
303+
*/
304+
char *
305+
PQcancelErrorMessage(const PGcancelConn *cancelConn)
306+
{
307+
return PQerrorMessage(&cancelConn->conn);
308+
}
309+
310+
/*
311+
* PQcancelReset
312+
*
313+
* Resets the cancel connection, so it can be reused to send a new cancel
314+
* request.
315+
*/
316+
void
317+
PQcancelReset(PGcancelConn *cancelConn)
318+
{
319+
pqClosePGconn(&cancelConn->conn);
320+
cancelConn->conn.status = CONNECTION_ALLOCATED;
321+
cancelConn->conn.whichhost = 0;
322+
cancelConn->conn.whichaddr = 0;
323+
cancelConn->conn.try_next_host = false;
324+
cancelConn->conn.try_next_addr = false;
325+
}
326+
327+
/*
328+
* PQcancelFinish
329+
*
330+
* Closes and frees the cancel connection.
331+
*/
332+
void
333+
PQcancelFinish(PGcancelConn *cancelConn)
334+
{
335+
PQfinish(&cancelConn->conn);
336+
}
337+
44338
/*
45339
* PQgetCancel: get a PGcancel structure corresponding to a connection.
46340
*
@@ -145,7 +439,7 @@ optional_setsockopt(int fd, int protoid, int optid, int value)
145439

146440

147441
/*
148-
* PQcancel: request query cancel
442+
* PQcancel: old, non-encrypted, but signal-safe way of requesting query cancel
149443
*
150444
* The return value is true if the cancel request was successfully
151445
* dispatched, false if not (in which case an error message is available).

0 commit comments

Comments
 (0)