Skip to content

fix: allow connect again after close #2355

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

Closed
wants to merge 12 commits into from
5 changes: 3 additions & 2 deletions .evergreen/install-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ NPM_TMP_DIR="${NODE_ARTIFACTS_PATH}/tmp"

# this needs to be explicitly exported for the nvm install below
export NVM_DIR="${NODE_ARTIFACTS_PATH}/nvm"
export XDG_CONFIG_HOME=${NODE_ARTIFACTS_PATH}

# create node artifacts path if needed
mkdir -p ${NODE_ARTIFACTS_PATH}
mkdir -p ${NPM_CACHE_DIR}
mkdir -p "${NPM_TMP_DIR}"

# install Node.js
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.35.3/install.sh | bash
[ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh"
nvm install --lts=${NODE_LTS_NAME}
nvm install --no-progress --lts=${NODE_LTS_NAME}
Copy link
Contributor Author

@emadum emadum May 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I upgraded nvm so we can use the newish --no-progress option.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh very nice 👏


# setup npm cache in a local directory
cat <<EOT > .npmrc
Expand Down
25 changes: 16 additions & 9 deletions lib/mongo_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,28 +241,35 @@ MongoClient.prototype.close = function(force, callback) {
force = false;
}

const client = this;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were using arrow functions everywhere here, so I just updated everything to use this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc the reason client was used is because this method has significant enough depth that this can't be bound to the deepest nested functions. Can you confirm, for instance, that assignment to autoEncrypter below is actually referencing the correct this?

const topology = this.topology;

return maybePromise(this, callback, cb => {
const completeClose = err => {
client.emit('close', client);
this.emit('close', this);

if (!(client.topology instanceof NativeTopology)) {
for (const item of client.s.dbCache) {
item[1].emit('close', client);
if (!(topology instanceof NativeTopology)) {
for (const item of this.s.dbCache) {
item[1].emit('close', this);
}
}

client.removeAllListeners('close');
this.removeAllListeners('close');

// clear out references to old topology
this.topology = undefined;
this.s.dbCache = new Map();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the old dbCache still references the old topology, so it is manually cleared out here in addition to this.topology

this.s.sessions = new Set();

cb(err);
};

if (client.topology == null) {
if (topology == null) {
completeClose();
return;
}

client.topology.close(force, err => {
const autoEncrypter = client.topology.s.options.autoEncrypter;
topology.close(force, err => {
const autoEncrypter = topology.s.options.autoEncrypter;
if (!autoEncrypter) {
completeClose(err);
return;
Expand Down
5 changes: 5 additions & 0 deletions lib/operations/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ function connect(mongoClient, url, options, callback) {
throw new Error('no callback function provided');
}

// Has a connection already been established?
if (mongoClient.topology && mongoClient.topology.isConnected()) {
throw new Error(`'connect' cannot be called when already connected`);
}

let didRequestAuthentication = false;
const logger = Logger('MongoClient', options);

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"test": "npm run lint && mocha --recursive test/functional test/unit test/core",
"test-nolint": "mocha --recursive test/functional test/unit test/core",
"coverage": "istanbul cover mongodb-test-runner -- -t 60000 test/core test/unit test/functional",
"lint": "eslint lib test",
"lint": "eslint -v && eslint lib test",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR. I just sometimes get weird discrepancies between running eslint locally and what runs on evergreen. Was thinking adding this could help make things a bit more explicit around the version of eslint being used, when it is being used.

"format": "prettier --print-width 100 --tab-width 2 --single-quote --write 'test/**/*.js' 'lib/**/*.js'",
"bench": "node test/benchmarks/driverBench/",
"generate-evergreen": "node .evergreen/generate_evergreen_tasks.js",
Expand Down
102 changes: 55 additions & 47 deletions test/functional/change_stream.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ var Transform = require('stream').Transform;
const MongoError = require('../../lib/core').MongoError;
var MongoNetworkError = require('../../lib/core').MongoNetworkError;
var setupDatabase = require('./shared').setupDatabase;
var withTempDb = require('./shared').withTempDb;
var withClient = require('./shared').withClient;
var withDb = require('./shared').withDb;
var withCollection = require('./shared').withCollection;
var delay = require('./shared').delay;
var co = require('co');
var mock = require('mongodb-mock-server');
Expand Down Expand Up @@ -59,8 +61,12 @@ function tryNext(changeStream, callback) {
let complete = false;
function done(err, result) {
if (complete) return;
// if the arity is 1 then this a callback for `more`
// if the arity is 1 then this is a callback for `more`
if (arguments.length === 1) {
if (err instanceof Error) {
callback(err);
return;
}
result = err;
const batch = result.cursor.firstBatch || result.cursor.nextBatch;
if (batch.length === 0) {
Expand Down Expand Up @@ -2632,65 +2638,67 @@ describe('Change Streams', function() {
});

describe('tryNext', function() {
function withTemporaryCollectionOnDb(database, testFn) {
return function() {
return withClient.bind(this)(
withDb(
database,
{ helper: { drop: true } },
withCollection(
{
collection: { w: 'majority' },
helper: { create: true }
},
testFn
)
)
);
};
}
it('should return null on single iteration of empty cursor', {
metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } },
test: function() {
return withTempDb(
'testTryNext',
{ w: 'majority' },
this.configuration.newClient(),
db => done => {
const changeStream = db.collection('test').watch();
tryNext(changeStream, (err, doc) => {
expect(err).to.not.exist;
expect(doc).to.not.exist;
test: withTemporaryCollectionOnDb('test_try_next_empty', (collection, done) => {
const changeStream = collection.watch();
tryNext(changeStream, (err, doc) => {
expect(err).to.not.exist;
expect(doc).to.not.exist;

changeStream.close(done);
});
}
);
}
changeStream.close(done);
});
})
});

it('should iterate a change stream until first empty batch', {
metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } },
test: function() {
return withTempDb(
'testTryNext',
{ w: 'majority' },
this.configuration.newClient(),
db => done => {
const collection = db.collection('test');
const changeStream = collection.watch();
waitForStarted(changeStream, () => {
collection.insertOne({ a: 42 }, err => {
expect(err).to.not.exist;
test: withTemporaryCollectionOnDb('test_try_next_results', (collection, done) => {
const changeStream = collection.watch();
waitForStarted(changeStream, () => {
collection.insertOne({ a: 42 }, err => {
expect(err).to.not.exist;

collection.insertOne({ b: 24 }, err => {
expect(err).to.not.exist;
});
});
collection.insertOne({ b: 24 }, err => {
expect(err).to.not.exist;
});
});
});

tryNext(changeStream, (err, doc) => {
expect(err).to.not.exist;
expect(doc).to.exist;
tryNext(changeStream, (err, doc) => {
expect(err).to.not.exist;
expect(doc).to.exist;

tryNext(changeStream, (err, doc) => {
expect(err).to.not.exist;
expect(doc).to.exist;
tryNext(changeStream, (err, doc) => {
expect(err).to.not.exist;
expect(doc).to.exist;

tryNext(changeStream, (err, doc) => {
expect(err).to.not.exist;
expect(doc).to.not.exist;
tryNext(changeStream, (err, doc) => {
expect(err).to.not.exist;
expect(doc).to.not.exist;

changeStream.close(done);
});
});
changeStream.close(done);
});
}
);
}
});
});
})
});
});

Expand Down
30 changes: 27 additions & 3 deletions test/functional/connection.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict';
const test = require('./shared').assert,
setupDatabase = require('./shared').setupDatabase,
expect = require('chai').expect;
const test = require('./shared').assert;
const setupDatabase = require('./shared').setupDatabase;
const withClient = require('./shared').withClient;
const expect = require('chai').expect;

describe('Connection', function() {
before(function() {
Expand Down Expand Up @@ -514,4 +515,27 @@ describe('Connection', function() {
});
}
});

it('should be able to connect again after close', function() {
return withClient.call(this, (client, done) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be equivalent to just calling withClient within the scope, why was this change needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without it I get TypeError: Cannot read property 'configuration' of undefined referencing this line.

expect(client.isConnected()).to.be.true;
const collection = () => client.db('testReconnect').collection('test');
collection().insertOne({ a: 1 }, (err, result) => {
expect(err).to.not.exist;
expect(result).to.exist;
client.close(err => {
expect(err).to.not.exist;
client.connect(err => {
expect(err).to.not.exist;
collection().insertOne({ b: 2 }, (err, result) => {
expect(err).to.not.exist;
expect(result).to.exist;
expect(client.topology.isDestroyed()).to.be.false;
done();
});
});
});
});
});
});
});
4 changes: 2 additions & 2 deletions test/functional/sessions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('Sessions', function() {
// verify that the `endSessions` command was sent
const lastCommand = test.commands.started[test.commands.started.length - 1];
expect(lastCommand.commandName).to.equal('endSessions');
expect(client.topology.s.sessionPool.sessions).to.have.length(0);
expect(client.topology).to.not.exist;
});
});
});
Expand All @@ -149,7 +149,7 @@ describe('Sessions', function() {
// verify that the `endSessions` command was sent
const lastCommand = test.commands.started[test.commands.started.length - 1];
expect(lastCommand.commandName).to.equal('endSessions');
expect(client.topology.s.sessionPool.sessions).to.have.length(0);
expect(client.topology).to.not.exist;
});
});
}
Expand Down
Loading