diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 1758e80c24..42f3b1188a 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -1039,9 +1039,7 @@ export abstract class AbstractCursor< this.selectedServer && !this.cursorSession.hasEnded ) { - this.isKilled = true; const cursorId = this.cursorId; - this.cursorId = Long.ZERO; await executeOperation( this.cursorClient, @@ -1061,6 +1059,8 @@ export abstract class AbstractCursor< if (!this.cursorSession?.inTransaction()) { maybeClearPinnedConnection(this.cursorSession, { error }); } + this.cursorId = Long.ZERO; + this.isKilled = true; } finally { this.emitClose(); } diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 0743cd211d..b5fcc17aec 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,8 +1,11 @@ /* eslint-disable @typescript-eslint/no-empty-function */ +import { expect } from 'chai'; + +import { type Collection, type FindCursor, type MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; -describe.skip('MongoClient.close() Integration', () => { +describe('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree let config: TestConfiguration; @@ -13,7 +16,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('Node.js resource: TLS File read', () => { describe('when client is connecting and reads an infinite TLS file', () => { - it('the file read is interrupted by client.close()', async function () { + it.skip('the file read is interrupted by client.close()', async function () { await runScriptAndGetProcessInfo( 'tls-file-read', config, @@ -49,7 +52,7 @@ describe.skip('MongoClient.close() Integration', () => { }); describe('when MongoClientAuthProviders is instantiated and token file read hangs', () => { - it('the file read is interrupted by client.close()', async () => { + it.skip('the file read is interrupted by client.close()', async () => { await runScriptAndGetProcessInfo( 'token-file-read', config, @@ -76,8 +79,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('Node.js resource: Server Selection Timer', () => { describe('after a Topology is created through client.connect()', () => { const metadata: MongoDBMetadataUI = { requires: { topology: 'replicaset' } }; - - it('server selection timers are cleaned up by client.close()', metadata, async () => { + it.skip('server selection timers are cleaned up by client.close()', metadata, async () => { const run = async function ({ MongoClient, uri, expect, sleep, mongodb, getTimerCount }) { const serverSelectionTimeoutMS = 2222; const client = new MongoClient(uri, { @@ -116,7 +118,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('MonitorInterval', () => { describe('Node.js resource: Timer', () => { describe('after a new monitor is made', () => { - it( + it.skip( 'monitor interval timer is cleaned up by client.close()', metadata, async function () { @@ -149,7 +151,7 @@ describe.skip('MongoClient.close() Integration', () => { }); describe('after a heartbeat fails', () => { - it( + it.skip( 'the new monitor interval timer is cleaned up by client.close()', metadata, async () => { @@ -159,7 +161,6 @@ describe.skip('MongoClient.close() Integration', () => { const willBeHeartbeatFailed = once(client, 'serverHeartbeatFailed'); client.connect(); await willBeHeartbeatFailed; - function getMonitorTimer(servers) { for (const [, server] of servers) { return server?.monitor.monitorId.timerId; @@ -182,7 +183,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('Monitoring Connection', () => { describe('Node.js resource: Socket', () => { - it('no sockets remain after client.close()', metadata, async function () { + it.skip('no sockets remain after client.close()', metadata, async function () { const run = async function ({ MongoClient, uri, expect, getSocketEndpoints }) { const client = new MongoClient(uri); await client.connect(); @@ -210,7 +211,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('RTT Pinger', () => { describe('Node.js resource: Timer', () => { describe('after entering monitor streaming mode ', () => { - it( + it.skip( 'the rtt pinger timer is cleaned up by client.close()', metadata, async function () { @@ -246,8 +247,8 @@ describe.skip('MongoClient.close() Integration', () => { describe('Connection', () => { describe('Node.js resource: Socket', () => { describe('when rtt monitoring is turned on', () => { - it('no sockets remain after client.close()', metadata, async () => { - const run = async ({ MongoClient, uri, expect, getSockets, once, log }) => { + it.skip('no sockets remain after client.close()', metadata, async () => { + const run = async ({ MongoClient, uri, expect, getSockets, once }) => { const heartbeatFrequencyMS = 500; const client = new MongoClient(uri, { serverMonitoringMode: 'stream', @@ -264,7 +265,6 @@ describe.skip('MongoClient.close() Integration', () => { while (heartbeatOccurredSet.size < servers.size) { const ev = await once(client, 'serverHeartbeatSucceeded'); - log({ ev: ev[0] }); heartbeatOccurredSet.add(ev[0].connectionId); } @@ -280,8 +280,6 @@ describe.skip('MongoClient.close() Integration', () => { // close the client await client.close(); - - log({ socketsAfterClose: getSockets() }); // upon close, assert rttPinger sockets are cleaned up const activeSocketsAfterClose = activeSocketsAfterHeartbeat(); expect(activeSocketsAfterClose).to.have.lengthOf(0); @@ -298,7 +296,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('ConnectionPool', () => { describe('Node.js resource: minPoolSize timer', () => { describe('after new connection pool is created', () => { - it('the minPoolSize timer is cleaned up by client.close()', async function () { + it.skip('the minPoolSize timer is cleaned up by client.close()', async function () { const run = async function ({ MongoClient, uri, expect, getTimerCount }) { const client = new MongoClient(uri, { minPoolSize: 1 }); let minPoolSizeTimerCreated = false; @@ -356,7 +354,7 @@ describe.skip('MongoClient.close() Integration', () => { await utilClient.close(); }); - it('the wait queue timer is cleaned up by client.close()', async function () { + it.skip('the wait queue timer is cleaned up by client.close()', async function () { const run = async function ({ MongoClient, uri, expect, getTimerCount, once }) { const waitQueueTimeoutMS = 1515; @@ -398,7 +396,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('Connection', () => { describe('Node.js resource: Socket', () => { describe('after a minPoolSize has been set on the ConnectionPool', () => { - it('no sockets remain after client.close()', async function () { + it.skip('no sockets remain after client.close()', async function () { const run = async function ({ MongoClient, uri, expect, getSockets }) { // assert no sockets to start with expect(getSockets()).to.have.lengthOf(0); @@ -430,13 +428,17 @@ describe.skip('MongoClient.close() Integration', () => { const metadata: MongoDBMetadataUI = { requires: { topology: 'sharded' } }; describe('after SRVPoller is created', () => { - it('timers are cleaned up by client.close()', metadata, async () => { + it.skip('timers are cleaned up by client.close()', metadata, async () => { const run = async function ({ MongoClient, expect, getTimerCount }) { const SRV_CONNECTION_STRING = `mongodb+srv://test1.test.build.10gen.cc`; + // 27018 localhost.test.build.10gen.cc. // 27017 localhost.test.build.10gen.cc. - const client = new MongoClient(SRV_CONNECTION_STRING); + const client = new MongoClient(SRV_CONNECTION_STRING, { + serverSelectionTimeoutMS: 2000, + tls: false + }); await client.connect(); // the current expected behavior is that _timeout is set to undefined until SRV polling starts // then _timeout is set to undefined again when SRV polling stops @@ -452,29 +454,143 @@ describe.skip('MongoClient.close() Integration', () => { }); describe('ClientSession (Implicit)', () => { + let idleSessionsBeforeClose; + let idleSessionsAfterClose; + let client; + let utilClient; + let session; + + const metadata: MongoDBMetadataUI = { + requires: { + topology: ['replicaset', 'sharded'], + mongodb: '>=4.2' + } + }; + + beforeEach(async function () { + client = this.configuration.newClient(); + utilClient = this.configuration.newClient(); + await client.connect(); + await client + .db('db') + .collection('collection') + ?.drop() + .catch(() => null); + const collection = await client.db('db').createCollection('collection'); + session = client.startSession({ explicit: false }); + session.startTransaction(); + await collection.insertOne({ x: 1 }, { session }); + + const opBefore = await utilClient.db().admin().command({ currentOp: 1 }); + idleSessionsBeforeClose = opBefore.inprog.filter(s => s.type === 'idleSession'); + + await client.close(); + + const opAfter = await utilClient.db().admin().command({ currentOp: 1 }); + idleSessionsAfterClose = opAfter.inprog.filter(s => s.type === 'idleSession'); + + await utilClient.close(); + }); + + afterEach(async function () { + await utilClient?.close(); + await session?.endSession(); + await client?.close(); + }); + describe('Server resource: LSID/ServerSession', () => { describe('after a clientSession is implicitly created and used', () => { - it.skip('the server-side ServerSession is cleaned up by client.close()', async function () {}); + it( + 'the server-side ServerSession is cleaned up by client.close()', + metadata, + async function () { + expect(idleSessionsBeforeClose).to.not.be.empty; + expect(idleSessionsAfterClose).to.be.empty; + } + ); }); }); describe('Server resource: Transactions', () => { describe('after a clientSession is implicitly created and used', () => { - it.skip('the server-side transaction is cleaned up by client.close()', async function () {}); + it( + 'the server-side transaction is cleaned up by client.close()', + metadata, + async function () { + expect(idleSessionsBeforeClose[0].transaction.txnNumber).to.not.null; + expect(idleSessionsAfterClose).to.be.empty; + } + ); }); }); }); describe('ClientSession (Explicit)', () => { + let idleSessionsBeforeClose; + let idleSessionsAfterClose; + let client; + let utilClient; + let session; + + const metadata: MongoDBMetadataUI = { + requires: { + topology: ['replicaset', 'sharded'], + mongodb: '>=4.2' + } + }; + + beforeEach(async function () { + client = this.configuration.newClient(); + utilClient = this.configuration.newClient(); + await client.connect(); + await client + .db('db') + .collection('collection') + ?.drop() + .catch(() => null); + const collection = await client.db('db').createCollection('collection'); + session = client.startSession(); + session.startTransaction(); + await collection.insertOne({ x: 1 }, { session }); + + const opBefore = await utilClient.db().admin().command({ currentOp: 1 }); + idleSessionsBeforeClose = opBefore.inprog.filter(s => s.type === 'idleSession'); + + await client.close(); + + const opAfter = await utilClient.db().admin().command({ currentOp: 1 }); + idleSessionsAfterClose = opAfter.inprog.filter(s => s.type === 'idleSession'); + }); + + afterEach(async function () { + await utilClient?.close(); + await session?.endSession(); + await client?.close(); + }); + describe('Server resource: LSID/ServerSession', () => { describe('after a clientSession is created and used', () => { - it.skip('the server-side ServerSession is cleaned up by client.close()', async function () {}); + it( + 'the server-side ServerSession is cleaned up by client.close()', + metadata, + async function () { + expect(idleSessionsBeforeClose).to.not.be.empty; + expect(idleSessionsAfterClose).to.be.empty; + } + ); }); }); describe('Server resource: Transactions', () => { describe('after a clientSession is created and used', () => { - it.skip('the server-side transaction is cleaned up by client.close()', async function () {}); + it( + 'the server-side transaction is cleaned up by client.close()', + metadata, + async function () { + expect(idleSessionsBeforeClose[0].transaction.txnNumber).to.not.null; + expect(idleSessionsAfterClose).to.be.empty; + } + ); }); }); }); @@ -490,7 +606,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('KMS Request', () => { describe('Node.js resource: TLS file read', () => { describe('when KMSRequest reads an infinite TLS file', () => { - it('the file read is interrupted by client.close()', metadata, async () => { + it.skip('the file read is interrupted by client.close()', metadata, async () => { await runScriptAndGetProcessInfo( 'tls-file-read-auto-encryption', config, @@ -583,8 +699,75 @@ describe.skip('MongoClient.close() Integration', () => { }); describe('Server resource: Cursor', () => { + const metadata: MongoDBMetadataUI = { + requires: { + mongodb: '>=4.2.0' + } + }; + describe('after cursors are created', () => { - it.skip('all active server-side cursors are closed by client.close()', async function () {}); + let client: MongoClient; + let coll: Collection; + let cursor: FindCursor; + let utilClient: MongoClient; + + beforeEach(async function () { + client = this.configuration.newClient(); + utilClient = this.configuration.newClient(); + await client.connect(); + await client + .db('db') + .collection('coll') + ?.drop() + .catch(() => null); + coll = await client + .db('db') + .createCollection('coll', { capped: true, size: 1_000, max: 4 }); + await coll.insertMany([{ a: 1 }, { b: 2 }, { c: 3 }]); + }); + + afterEach(async function () { + await utilClient?.close(); + await client?.close(); + await cursor?.close(); + }); + + it( + 'all active server-side cursors are closed by client.close()', + metadata, + async function () { + const getCursors = async () => { + const res = await utilClient + .db() + .admin() + .command({ + aggregate: 1, + cursor: {}, + pipeline: [{ $currentOp: { idleCursors: true } }] + }); + return res.cursor.firstBatch.filter( + r => r.type === 'idleCursor' || (r.type === 'op' && r.desc === 'getMore') + ); + }; + + cursor = coll.find( + {}, + { + tailable: true, + awaitData: true + } + ); + await cursor.next(); + + // assert creation + expect(await getCursors()).to.not.be.empty; + + await client.close(); + + // assert clean-up + expect(await getCursors()).to.be.empty; + } + ); }); }); });