-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
query cancel #1392
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
query cancel #1392
Conversation
var Query = helper.pg.Query | ||
|
||
// before running this test make sure you run the script create-test-tables | ||
test('simple query interface', function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test doesn’t accept a done
callback or return a promise, so its assertions won’t fail at the right time. The assert(true)
can’t fail at all and doesn’t confirm that row
is called. Both queries are queued, so the important cancellation path isn’t tested at all. (Actually, the first one is queued too, because the test doesn’t wait for the client to connect.)
Queries spliced out of the queue should probably reject/emit an error like they would if cancelled while running. Then this test can use the promise API and:
- wait for the connection to complete
- start a query, asserting that it resolves
- start two more queries, cancelling them both and asserting that both reject
You are right! both queries are queued :-( |
lib/client.js
Outdated
@@ -379,6 +377,8 @@ Client.prototype.query = function (config, values, callback) { | |||
rejectOut = reject | |||
}) | |||
query.callback = (err, res) => err ? rejectOut(err) : resolveOut(res) | |||
} else { | |||
result = query |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn’t be added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need a reference to the query to be able to cancel it later. Is this breaking something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, it was removed intentionally and doesn’t work with promises. I’m not sure what the right fix is, though… requiring the Query
object to be passed explicitly like in the tests? Or a separate method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My idea was that if you want to cancel a request, you don't use the promise api. Most of users don't cancel queries, the few people who need to cancel will have to use the callback api. That seemed to me to be the best tradeoff.
In my application I have written a wrapper around the callback api to use promises. Cancelable queries resolves with something like:
{
data: Promise { <pending> },
cancel: [Function: bound cancel]
}
But this is more complex for users; the cancelable queries api become different from the "normal" queries; and maybe it's not the pg responsibility to do this kind of things (too high level?). And the wrapper itself needs to keep references to query...
Do you remember why returning the query was removed?
@charmander have you seen my reply to your comment? |
I don't think we need to change the return type of That would still require an extra step for a user's queries to be cancellable (i.e. use Query instead of just a string) but as you've noted, that's the exception not the common case. Most people don't cancel their queries. On the plus side it'd be pool friendly as the |
We can't use the connection of the query to cancel it (see the postgresql doc https://www.postgresql.org/docs/9.3/static/protocol-flow.html#AEN99752). We have to open a new connection. Can you explain me what is the problem with the return value of .query? |
I'm not saying to reuse the connection. I'm saying the Query object would have a
The previous work for 7.0 (see #1299) normalized the return type of
The return type also shouldn't very depending on whether it's invoked a pooled connection or a stand alone Client. Your PR would break that up again with a special case for cancel. Rather than supporting it as a special case, I'm suggesting we keep the current behavior and expose the method through the Query object itself. It'd likely require some internal work to add the hooks but longer term I think keeping the |
Yes something like that. Haven't had a chance to really review it yet though (just eyeballed it a bit). I wonder if it'd be worth having the Query keep track of whether it actually has completed prior to issuing the cancel. PostgreSQL is kind of annoying in that cancellation isn't tied to queries, it's a basic message that says, "Cancel whatever connection XYZ is doing", even if the original bad/slow/hanging action is no longer the one that's running. It's impossible to completely work around that race condition but we could make it a bit more robust by having the Query check if it's still running on the client side prior to issuing the cancel. |
From the postgres server point of view, it's not a problem
So we can assume that when canceling a request, the user must not expect the |
I mean that it's a problem for an app relying on a cancel impacting a specific query. If you're using a pooled connection it can be a problem because you can end up cancelling a different command subsequently executed using the same client. Example:
Now there's no way to avoid this because of how the v3 protocol works but you can slightly avoid it by ignoring the cancel request if we know the query is already finished. Specifically if step 3 above happens after the client is notified of the completion (i.e. step 4) then we can safely skip the cancel. Assuming it's meant to be query driven and not client driven. |
native version added |
You are right, but we cannot do a lot to avoid it. Canceling a query is an exception, and your case is an exception of the exception :-) connection.on('connect', function () {
if (runningClient.activeQuery === self) {
connection.cancel(self.client.processID, self.client.secretKey)
}
}) I think it's the latest we can do it. Or can we do better? |
@sehrope @charmander any progress on this issue? we need to fix this regression... |
lib/query.js
Outdated
const connection = cancelingClient.connection | ||
connection.connect(runningClient.port, runningClient.host) | ||
const self = this | ||
connection.on('connect', function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could use arrow function and remove const self = this
.
reject(); | ||
}) | ||
query1.on('error', (err) => { | ||
assert.equal(err.code, 57014); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could define the error code above in a constant, so that it's not so much of a magic number.
Thank for your review @nicolasroger17 |
You're welcome. |
@brianc @sehrope @charmander At least can you tell us what is the problem? |
Don't usually like to +1 or ping, but this ticket has been open for a while. Anything I can help out with to get this merged? We're spending a lot of $$$ on RDS bills for queries that could be killed when a request ends before the query is done (user closes the page, clicks around, etc.). |
Sorry to raise this after the API has already been rewritten once, but: I really think cancellation should, to prevent surprises, reflect the way it actually works – i.e. on a connection, not a query. If my understanding is correct, both the original implementation and this PR suffer from a race condition that could end up cancelling an arbitrary future query made by the client instead of the one explicitly referenced. So, proposed alternative:
Then, to make typical use convenient and correct,
making it explicit that what’s cancelled is whatever query the client is executing when the message reaches it while allowing pools to continue working. There’s some extra overhead in making a new full connection to send the message, but reduced overhead if the pool has a free client. This should definitely be tested with SSL, too! I will try adding that to CI, and am up for implementing this afterwards if people think it’s a good idea. (@sehrope?) @contra I’m not sure that brianc is available anyway, though, so in the meantime you can approximate this by getting the PID in advance for each client running a query you’d potentially want to cancel with |
+1
Yes that's a problem with PostgreSQL query cancellation in general, not just node-postgres. To issue a cancel you specify only the process ID of the backend executing the query you'd like to cancel. You don't specify anything specific to the query itself. Whatever happens to be running at the time the cancel gets processed will be cancelled. That could be the original query (if it's still running), nothing (if it finished and the connection is idle), or some subsequently executed query. The query cancellation is also asynchronous on the backend so the completion of the issuance of the request doesn't mean anything was (or will be) cancelled. The issuer of the cancel doesn't get any feedback whatsoever: https://www.postgresql.org/docs/current/static/protocol-flow.html#idm46428663888448
For the cancellation itself you have two choices. You can either use a different connection and issue a
Mixing cancellation and connection pools is pretty messy. It's likely doable but there's a lot of bookkeeping involved to keep things consistent. This gets particularly messy due to the client query queue (in memory, not sent to the backend) and the pool handing out raw connections (so after pool return you can shoot yourself in the foot if you try to reuse the connection).
Per above, most other drivers open a new connection for cancellation. Presumably cancellation isn't something that happens frequently enough to warrant a connection pool. Plus using a pooled connection runs the risk of the connection being tainted by something else (ex: dangingly errant transaction) which would cause the
Yes you have to discard it. I don't even think you can track the error responses on the original connection as it's possible that someone else cancelled the query. The only safe thing to do with a cancelled pooled connection is to close it.
A better separation of the client / pool would simplify a lot of this. See #1414 (comment) for some detail on what I mean. With that approach the
+1. SSL all the things!
I want to take a stab at the Client / PoolClient separation I mentioned above as I think it'd simplify implementing a saner
+1 to this approach if you want more fine grained control over the cancellation. I'd also suggest manually managing your connections. You can still use the pool as a source of connections but do explicit checkout / checkin so that you only return connections that you're done with and know for certain will not have async cancels invoked on them. Anything that's been cancelled should be discarded. Or just use explicit clients without pools. |
The idea was to send a cancellation message on a regularly-connected client to allow that client to be taken from and returned to the pool… if it’s valid to send that message after a normal startup. I didn’t check. But this was only to allow…
… limits to be enforced if the cancellation could be triggered by some outside action. Maybe the user can handle that.
(This is a pool bug, though.)
At least that makes the implementation straightforward! Thanks.
Very much looking forward to this. I’ll do that SSL CI thing in the meantime, then. |
Any updates? there's still no way to cancel the query... |
@arthurfiorette There’s still |
@arthurfiorette honestly what @charmander is suggesting is almost as good as a built-in cancel function. read up on postgres and how it does (or doesn't) cancel queries & you'll see how. I'm going to implement this evenually but it's not high priority given how race-condition prone and edge-case canceling queries in code is (obviously you want to cancel queries in code but a built in cancel funciton is SO fraught with edge cases you might as well just send a cancel query in on an avail client) |
The cancel method seems not to be working anymore, and the test has disappeared.
ps: the new documentation node-postgres.com is great! congratulations. If you need it I can write some doc on client.cancel