Skip to content
This repository was archived by the owner on May 17, 2021. It is now read-only.

Commit 0f29535

Browse files
authored
fix: Use latest connection-model and keep reference to tunnel to be able to close it when disconnecting native client COMPASS-4474 (#308)
* fix: Use latest connection-model and keep reference to tunnel to be able to close it when disconnecting native client COMPASS-4474 * feat: Do not allow to call connect multiple times without disconnecting first * test: Test that we are closing the tunnel before resolving the disconnect callback
1 parent 791ba8a commit 0f29535

File tree

4 files changed

+182
-61
lines changed

4 files changed

+182
-61
lines changed

lib/native-client.js

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,13 @@ class NativeClient extends EventEmitter {
9292
constructor(model) {
9393
super();
9494
this.model = model;
95+
9596
this.connectionOptions = null;
97+
this.tunnel = null;
98+
99+
this.isWritable = false;
100+
this.isMongos = false;
101+
this._isConnected = false;
96102
}
97103

98104
/**
@@ -104,19 +110,33 @@ class NativeClient extends EventEmitter {
104110
connect(done) {
105111
debug('connecting...');
106112

107-
this.connectionOptions = null;
108-
this.isWritable = false;
109-
this.isMongos = false;
113+
if (this._isConnected) {
114+
setImmediate(() => {
115+
done(
116+
new Error(
117+
'Connect method has been called more than once without disconnecting.'
118+
)
119+
);
120+
});
121+
122+
return this;
123+
}
124+
125+
// Not really true at that point, we are doing it just so we don't allow
126+
// simultaneous syncronous calls to the connect method
127+
this._isConnected = true;
110128

111129
connect(
112130
this.model,
113131
this.setupListeners.bind(this),
114-
(err, _client, connectionOptions) => {
132+
(err, _client, tunnel, connectionOptions) => {
115133
if (err) {
134+
this._isConnected = false;
116135
return done(this._translateMessage(err));
117136
}
118137

119138
this.connectionOptions = connectionOptions;
139+
this.tunnel = tunnel;
120140

121141
this.isWritable = this.client.isWritable;
122142
this.isMongos = this.client.isMongos;
@@ -128,6 +148,7 @@ class NativeClient extends EventEmitter {
128148

129149
this.client.on('status', (evt) => this.emit('status', evt));
130150
this.database = this.client.db(this.model.ns || ADMIN);
151+
131152
done(null, this);
132153
}
133154
);
@@ -298,6 +319,8 @@ class NativeClient extends EventEmitter {
298319
*
299320
* @param {String} databaseName - The database name.
300321
* @param {Function} callback - The callback.
322+
*
323+
* @returns {void}
301324
*/
302325
collections(databaseName, callback) {
303326
if (databaseName === SYSTEM) {
@@ -521,9 +544,22 @@ class NativeClient extends EventEmitter {
521544

522545
/**
523546
* Disconnect the client.
547+
* @param {Function} callback
524548
*/
525549
disconnect(callback) {
526-
this.client.close(true, callback);
550+
this.client.close(true, err => {
551+
if (this.tunnel) {
552+
debug('mongo client closed. shutting down ssh tunnel');
553+
this.tunnel.close().finally(() => {
554+
this._cleanup();
555+
debug('ssh tunnel stopped');
556+
callback(err);
557+
});
558+
} else {
559+
this._cleanup();
560+
return callback(err);
561+
}
562+
});
527563
}
528564

529565
/**
@@ -1249,6 +1285,15 @@ class NativeClient extends EventEmitter {
12491285
}
12501286
return error;
12511287
}
1288+
1289+
_cleanup() {
1290+
this.client = null;
1291+
this.connectionOptions = null;
1292+
this.tunnel = null;
1293+
this.isWritable = false;
1294+
this.isMongos = false;
1295+
this._isConnected = false;
1296+
}
12521297
}
12531298

12541299
function addDebugToClass(cls) {

package-lock.json

Lines changed: 22 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
],
3030
"peerDependencies": {
3131
"mongodb": "3.x",
32-
"mongodb-connection-model": "*"
32+
"mongodb-connection-model": ">=19.1.0"
3333
},
3434
"dependencies": {
3535
"async": "^3.2.0",
@@ -52,7 +52,7 @@
5252
"mocha": "^8.2.1",
5353
"mock-require": "^3.0.3",
5454
"mongodb": "^3.6.3",
55-
"mongodb-connection-model": "^18.0.0",
55+
"mongodb-connection-model": "^19.1.0",
5656
"mongodb-runner": "^4.8.0",
5757
"pre-commit": "^1.2.2",
5858
"sinon": "^9.2.3",

test/native-client.test.js

Lines changed: 108 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,58 @@ const EventEmitter = require('events');
88

99
var NativeClient = require('../lib/native-client');
1010

11+
function mockedTopologyDescription(
12+
topologyType = 'Standalone',
13+
serverType = 'Single'
14+
) {
15+
return {
16+
type: topologyType,
17+
servers: new Map([['127.0.0.1:27017', { type: serverType }]])
18+
};
19+
}
20+
21+
/*
22+
* pretends to be a connection-model providing every function call
23+
* required in NativeClient#connect, but returns topology and connection
24+
* params of our choice
25+
*/
26+
function mockedConnectionModel(topologyDescription, connectionOptions) {
27+
const _topologyDescription =
28+
topologyDescription || mockedTopologyDescription();
29+
30+
const _connectionOptions = connectionOptions || {
31+
url:
32+
'mongodb://127.0.0.1:27018/data-service?readPreference=primary&ssl=false',
33+
options: {
34+
connectWithNoPrimary: true,
35+
readPreference: 'primary',
36+
useNewUrlParser: true,
37+
useUnifiedTopology: true
38+
}
39+
};
40+
41+
const mockedTunnel = {
42+
close() {
43+
return Promise.resolve();
44+
}
45+
};
46+
47+
return {
48+
connect(_model, setupListeners, cb) {
49+
const mockedClient = new EventEmitter();
50+
mockedClient.db = () => {};
51+
mockedClient.close = (_force, closeCb) => {
52+
closeCb();
53+
};
54+
setupListeners(mockedClient);
55+
mockedClient.emit('topologyDescriptionChanged', {
56+
newDescription: _topologyDescription
57+
});
58+
cb(null, mockedClient, mockedTunnel, _connectionOptions);
59+
}
60+
};
61+
}
62+
1163
describe('NativeClient', function() {
1264
this.slow(10000);
1365
this.timeout(20000);
@@ -34,52 +86,32 @@ describe('NativeClient', function() {
3486

3587
describe('#connect', function() {
3688
context('when mocking connection-model', function() {
37-
function mockedTopologyDescription(
38-
topologyType = 'Standalone',
39-
serverType = 'Single'
40-
) {
41-
return {
42-
type: topologyType,
43-
servers: new Map([['127.0.0.1:27017', { type: serverType }]])
44-
};
45-
}
46-
47-
/*
48-
* pretends to be a connection-model providing every function call
49-
* required in NativeClient#connect, but returns topology and connection
50-
* params of our choice
51-
*/
52-
function mockedConnectionModel(topologyDescription, connectionOptions) {
53-
const _topologyDescription =
54-
topologyDescription || mockedTopologyDescription();
55-
56-
const _connectionOptions = connectionOptions || {
57-
url: 'mongodb://127.0.0.1:27018/data-service?readPreference=primary&ssl=false',
58-
options: {
59-
connectWithNoPrimary: true,
60-
readPreference: 'primary',
61-
useNewUrlParser: true,
62-
useUnifiedTopology: true
63-
}
64-
};
65-
66-
return {
67-
connect(_model, setupListeners, cb) {
68-
const mockedClient = new EventEmitter();
69-
mockedClient.db = () => {};
70-
setupListeners(mockedClient);
71-
mockedClient.emit('topologyDescriptionChanged', {
72-
newDescription: _topologyDescription
73-
});
74-
cb(null, mockedClient, _connectionOptions);
75-
}
76-
};
77-
}
78-
7989
after(function() {
8090
mock.stop('mongodb-connection-model');
8191
});
8292

93+
it('does not allow to connect twice without disonnecting first', (done) => {
94+
mock(
95+
'mongodb-connection-model',
96+
mockedConnectionModel()
97+
);
98+
99+
const MockedNativeClient = mock.reRequire('../lib/native-client');
100+
const mockedClient = new MockedNativeClient(helper.connection);
101+
102+
mockedClient.connect(() => {});
103+
mockedClient.connect(err => {
104+
expect(err).to.be.instanceOf(Error);
105+
expect(err)
106+
.to.have.property('message')
107+
.match(
108+
/Connect method has been called more than once without disconnecting/
109+
);
110+
111+
done();
112+
});
113+
});
114+
83115
it('sets .connectionOptions after successful connection', function(done) {
84116
mock(
85117
'mongodb-connection-model',
@@ -183,6 +215,40 @@ describe('NativeClient', function() {
183215
});
184216
});
185217

218+
describe('#disconnect', () => {
219+
context('when mocking connection-model', () => {
220+
after(() => {
221+
mock.stop('mongodb-connection-model');
222+
});
223+
224+
it('should close tunnel before calling disconnect callback', (done) => {
225+
mock(
226+
'mongodb-connection-model',
227+
mockedConnectionModel()
228+
);
229+
230+
const MockedNativeClient = mock.reRequire('../lib/native-client');
231+
const mockedClient = new MockedNativeClient(helper.connection);
232+
233+
mockedClient.connect(() => {
234+
const closeSpy = sandbox.spy(mockedClient.tunnel, 'close');
235+
236+
const disconnectCallbackSpy = sandbox.spy(() => {
237+
try {
238+
expect(closeSpy).to.have.been.calledOnce;
239+
expect(closeSpy).to.have.been.calledBefore(disconnectCallbackSpy);
240+
done();
241+
} catch (err) {
242+
done(err);
243+
}
244+
});
245+
246+
mockedClient.disconnect(disconnectCallbackSpy);
247+
});
248+
});
249+
});
250+
});
251+
186252
describe('#command', function() {
187253
it('executes the command', function(done) {
188254
client.command('data-service', { ping: 1 }, function(error, result) {

0 commit comments

Comments
 (0)