-
Notifications
You must be signed in to change notification settings - Fork 5
fix: Wait for the tunnel to close before finishing client close COMPASS-4474 #352
Conversation
ce94374
to
905c181
Compare
e97ccc6
to
e4c4576
Compare
…and fail) before server stops trying
lib/connect.js
Outdated
let maybeTunnelError; | ||
|
||
if (tunnel) { | ||
tunnel.on('error', (tunnelError) => { |
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.
tunnel.once('error'
?
lib/connect.js
Outdated
tunnel.close().then( | ||
() => { | ||
debug('ssh tunnel stopped'); | ||
cb(err); |
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.
cb(null)
?
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.
Now i get it, sorry .. this code is quite complex though .. i think we should give the promise version / or a more de-tangled approach a chance
@@ -12,7 +12,7 @@ const { | |||
const { MongoClient } = require('mongodb'); | |||
const { parseConnectionString } = require('mongodb/lib/core'); | |||
const Connection = require('./extended-model'); | |||
const createSSHTunnel = require('./ssh-tunnel'); | |||
const { default: SSHTunnel } = require('@mongodb-js/ssh-tunnel'); |
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.
😍
lib/connect.js
Outdated
tunnel.close().then( | ||
() => { | ||
debug('ssh tunnel stopped'); | ||
callbackOrPromise(clientError); |
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.
why we return an error here? Isn't this what is supposed to happen?
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 case is for when client failed to connect. We wait for tunnel to close and only then resolve the callback with the original client error
lib/connect.js
Outdated
} | ||
}); | ||
} | ||
|
||
mongoClient.connect((err, _client) => { |
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.
now that the tunnel uses promises, what about having an async function connect() {}
that we export with util.callbackify
instead than manually converting all of the promises call manually?
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 would not be a 1 to 1 replacement, connect
method synchronously returns state
emitter and additionally allows to pass a callback that will evaluate when connection happened, can't really replace this behavior with promises
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 really think we should avoid changing the client, let's investigate first some alternative with client.once('close') for example
After discussing F2F we decided to go with the exposing the tunnel from the connect method option |
…rough connect instead of overriding client.close 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.
Re-implemented as discussed, want to give it another look?
const _client = await Promise.race([ | ||
mongoClient.connect(), | ||
waitForTunnelError | ||
]); |
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.
Not really happy with this bit here as we are not really waiting for MongoClient.connect
to resolve if tunnel threw (this should not affect anything though, so I think it's okay as is), but that's more or less the only way to do it without doing weird callback stuff as it was in the previous implementation
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.
Good point!
Are user actionable errors about the tunnel thrown by .listen
?
The one thing I was assuming here is this: all the ssh related errors are thrown on tunnel.listen
ie if a cert or password is invalid .... and the only issues catched by on('error') are not caused by user mistakes (ie. they picked a wrong file) but by programming bug or network conditions that are not actionable beyond a retry: exception when the tcp connection is opened by MongoClient.
If that is true i think we can even ignore the race with waitForTunnelError
(just use it to make sure tunnel closes), since MongoClient will throw a connection error and we could just give a nice error in the UI: The tunnel was open correctly but the connection to mongodb failed: ${mongoError}
.
For sure we could give more info but is super fine, we are making it connect when it should, which now does not, we already improve the user life immensely.
If however instead errors like wrong password can only be catched by waitForTunnelError
than is a different story and probably we should wait for that as you did, or better make sure those errors are thrown by listen.
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.
all the ssh related errors are thrown on tunnel.listen ie if a cert or password is invalid
This doesn't happen until first connection happens, tunnel is established for every incoming connection to ssh tunnel server, so things like forward out are just not happening before that. If we want them to be thrown when listen
is called, some kind of ping request needs to be issued, which is doable. What do you think, should we add something like this here?
So in current implementation both errors emitted by tunnel or provided in callback can be helpful. But as mentioned, if tunnel emitted one, it probably will have a higher priority in terms of being useful to the user.
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.
forward out are just not happening before that
When it gets to forward the connection to ssh should already be established, isn't it?
tunnel is established for every incoming connection to ssh
why? can't we just both create a new tunnel and 'forwarding tcp server' on listen instead?
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.
why? can't we just both create a new tunnel and 'forwarding tcp server' on listen instead?
ssh2
docs don't recommend reusing one client for multiple connections, so @mongodb-js/ssh-tunnel creates new client for every incoming connection, which also means that client is not created until incoming connection is received
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.
Looks great! For what i remember race
may be returning an array, you may probably want to check that.
const _client = await Promise.race([ | ||
mongoClient.connect(), | ||
waitForTunnelError | ||
]); |
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.
Good point!
Are user actionable errors about the tunnel thrown by .listen
?
The one thing I was assuming here is this: all the ssh related errors are thrown on tunnel.listen
ie if a cert or password is invalid .... and the only issues catched by on('error') are not caused by user mistakes (ie. they picked a wrong file) but by programming bug or network conditions that are not actionable beyond a retry: exception when the tcp connection is opened by MongoClient.
If that is true i think we can even ignore the race with waitForTunnelError
(just use it to make sure tunnel closes), since MongoClient will throw a connection error and we could just give a nice error in the UI: The tunnel was open correctly but the connection to mongodb failed: ${mongoError}
.
For sure we could give more info but is super fine, we are making it connect when it should, which now does not, we already improve the user life immensely.
If however instead errors like wrong password can only be catched by waitForTunnelError
than is a different story and probably we should wait for that as you did, or better make sure those errors are thrown by listen.
cb(null, { url: model.driverUrlWithSsh, options: validOptions }); | ||
}); | ||
try { | ||
const _client = await Promise.race([ |
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.
isn't this: const [_client]
? is there a test that verifies if we return a client and a tunnel?
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.
Promise.race
returns only one value (it races a bunch of them against each other and only one "wins", right?), so it's not an array.
We have a whole suite of tests for connect
method and they are still passing, so client
is returned correctly, this implementation doesn't change current behavior. I haven't added any tests for returning tunnel
because I initially thought that this will require setting up a proper tunnel server to test the whole thing and it seemed like too much for a trivial test like that, but I can mock a bunch of things and write one for returning tunnel
, yeah
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.
Nice! makes a lot of sense :), could have checked myself.
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.
👍
Co-authored-by: Anna Henningsen <anna@addaleax.net>
Replaces
ssh-tunnel
implementation with@mongodb-js/ssh-tunnel
and safely overrides driverclose
* to always wait for tunnel to close before resolving the callback. That way we can always be sure that tunnel is completely stopped before allowing for another connection to be open through a tunnel with the same port in e.g., Compass* - I considered other options how to achieve that (for example, exposing
tunnel
with theclient
from theconnect
callback and then awaiting explicitly on it indata-service
), but overriding close on client still feels like a better balance between how much stuff is exposed from theconnection-model
and "hackiness" of the implementation