Skip to content

Commit e3d70c3

Browse files
authored
fix(NODE-6242): close becomes true after calling close when documents still remain (#4161)
1 parent fb724eb commit e3d70c3

File tree

4 files changed

+90
-10
lines changed

4 files changed

+90
-10
lines changed

src/cursor/abstract_cursor.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,13 @@ export abstract class AbstractCursor<
209209
}
210210
}
211211

212+
/**
213+
* The cursor has no id until it receives a response from the initial cursor creating command.
214+
*
215+
* It is non-zero for as long as the database has an open cursor.
216+
*
217+
* The initiating command may receive a zero id if the entire result is in the `firstBatch`.
218+
*/
212219
get id(): Long | undefined {
213220
return this.cursorId ?? undefined;
214221
}
@@ -249,10 +256,17 @@ export abstract class AbstractCursor<
249256
this.cursorSession = clientSession;
250257
}
251258

259+
/**
260+
* The cursor is closed and all remaining locally buffered documents have been iterated.
261+
*/
252262
get closed(): boolean {
253-
return this.isClosed;
263+
return this.isClosed && (this.documents?.length ?? 0) === 0;
254264
}
255265

266+
/**
267+
* A `killCursors` command was attempted on this cursor.
268+
* This is performed if the cursor id is non zero.
269+
*/
256270
get killed(): boolean {
257271
return this.isKilled;
258272
}
@@ -294,7 +308,7 @@ export abstract class AbstractCursor<
294308
return;
295309
}
296310

297-
if (this.closed && (this.documents?.length ?? 0) === 0) {
311+
if (this.closed) {
298312
return;
299313
}
300314

@@ -752,9 +766,11 @@ export abstract class AbstractCursor<
752766
!session.hasEnded
753767
) {
754768
this.isKilled = true;
769+
const cursorId = this.cursorId;
770+
this.cursorId = Long.ZERO;
755771
await executeOperation(
756772
this.cursorClient,
757-
new KillCursorsOperation(this.cursorId, this.cursorNamespace, this.selectedServer, {
773+
new KillCursorsOperation(cursorId, this.cursorNamespace, this.selectedServer, {
758774
session
759775
})
760776
);

test/integration/change-streams/change_stream.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1060,7 +1060,8 @@ describe('Change Streams', function () {
10601060
await changeStreamIterator.next();
10611061
await changeStreamIterator.return();
10621062
expect(changeStream.closed).to.be.true;
1063-
expect(changeStream.cursor).property('closed', true);
1063+
expect(changeStream.cursor).property('isClosed', true);
1064+
expect(changeStream.cursor).nested.property('session.hasEnded', true);
10641065
}
10651066
);
10661067

test/integration/node-specific/abstract_cursor.test.ts

+65-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type FindCursor,
1010
MongoAPIError,
1111
type MongoClient,
12+
MongoCursorExhaustedError,
1213
MongoServerError
1314
} from '../../mongodb';
1415

@@ -193,7 +194,9 @@ describe('class AbstractCursor', function () {
193194
const error = await cursor.toArray().catch(e => e);
194195

195196
expect(error).be.instanceOf(MongoAPIError);
196-
expect(cursor.closed).to.be.true;
197+
expect(cursor.id.isZero()).to.be.true;
198+
// The first batch exhausted the cursor, the only thing to clean up is the session
199+
expect(cursor.session.hasEnded).to.be.true;
197200
});
198201
});
199202

@@ -225,7 +228,9 @@ describe('class AbstractCursor', function () {
225228
}
226229
} catch (error) {
227230
expect(error).to.be.instanceOf(MongoAPIError);
228-
expect(cursor.closed).to.be.true;
231+
expect(cursor.id.isZero()).to.be.true;
232+
// The first batch exhausted the cursor, the only thing to clean up is the session
233+
expect(cursor.session.hasEnded).to.be.true;
229234
}
230235
});
231236
});
@@ -259,7 +264,9 @@ describe('class AbstractCursor', function () {
259264

260265
const error = await cursor.forEach(iterator).catch(e => e);
261266
expect(error).to.be.instanceOf(MongoAPIError);
262-
expect(cursor.closed).to.be.true;
267+
expect(cursor.id.isZero()).to.be.true;
268+
// The first batch exhausted the cursor, the only thing to clean up is the session
269+
expect(cursor.session.hasEnded).to.be.true;
263270
});
264271
});
265272
});
@@ -299,4 +306,59 @@ describe('class AbstractCursor', function () {
299306
expect(error).to.be.instanceof(MongoServerError);
300307
});
301308
});
309+
310+
describe('cursor end state', function () {
311+
let client: MongoClient;
312+
let cursor: FindCursor;
313+
314+
beforeEach(async function () {
315+
client = this.configuration.newClient();
316+
const test = client.db().collection('test');
317+
await test.deleteMany({});
318+
await test.insertMany([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]);
319+
});
320+
321+
afterEach(async function () {
322+
await cursor.close();
323+
await client.close();
324+
});
325+
326+
describe('when the last batch has been received', () => {
327+
it('has a zero id and is not closed and is never killed', async function () {
328+
cursor = client.db().collection('test').find({});
329+
expect(cursor).to.have.property('closed', false);
330+
await cursor.tryNext();
331+
expect(cursor.id.isZero()).to.be.true;
332+
expect(cursor).to.have.property('closed', false);
333+
expect(cursor).to.have.property('killed', false);
334+
});
335+
});
336+
337+
describe('when the last document has been iterated', () => {
338+
it('has a zero id and is closed and is never killed', async function () {
339+
cursor = client.db().collection('test').find({});
340+
await cursor.next();
341+
await cursor.next();
342+
await cursor.next();
343+
await cursor.next();
344+
expect(await cursor.next()).to.be.null;
345+
expect(cursor.id.isZero()).to.be.true;
346+
expect(cursor).to.have.property('closed', true);
347+
expect(cursor).to.have.property('killed', false);
348+
});
349+
});
350+
351+
describe('when some documents have been iterated and the cursor is closed', () => {
352+
it('has a zero id and is not closed and is killed', async function () {
353+
cursor = client.db().collection('test').find({}, { batchSize: 2 });
354+
await cursor.next();
355+
await cursor.close();
356+
expect(cursor).to.have.property('closed', false);
357+
expect(cursor).to.have.property('killed', true);
358+
expect(cursor.id.isZero()).to.be.true;
359+
const error = await cursor.next().catch(error => error);
360+
expect(error).to.be.instanceOf(MongoCursorExhaustedError);
361+
});
362+
});
363+
});
302364
});

test/integration/node-specific/cursor_async_iterator.test.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe('Cursor Async Iterator Tests', function () {
9595
}
9696

9797
expect(count).to.equal(1);
98-
expect(cursor.closed).to.be.true;
98+
expect(cursor.killed).to.be.true;
9999
});
100100

101101
it('cleans up cursor when breaking out of for await of loops', async function () {
@@ -106,7 +106,8 @@ describe('Cursor Async Iterator Tests', function () {
106106
break;
107107
}
108108

109-
expect(cursor.closed).to.be.true;
109+
// The expectation is that we have "cleaned" up the cursor on the server side
110+
expect(cursor.killed).to.be.true;
110111
});
111112

112113
it('returns when attempting to reuse the cursor after a break', async function () {
@@ -118,7 +119,7 @@ describe('Cursor Async Iterator Tests', function () {
118119
break;
119120
}
120121

121-
expect(cursor.closed).to.be.true;
122+
expect(cursor.killed).to.be.true;
122123

123124
for await (const doc of cursor) {
124125
expect.fail('Async generator returns immediately if cursor is closed', doc);

0 commit comments

Comments
 (0)