mongooseのfind query cursorは色々な回し方があるけどどれ使えばいいの?
mongooseのMongoDB query cursorは4つ書き方がある
1. cursorを自力で回す
const cursor = model.find().cursor()して、let doc = cursor.next()がnullになるまでforかwhileで回す
2. cursorを取り出しておいてfor await...ofで回す
const cursor = model.find().cursor()して、for await (const doc of cursor) { }で回す
3. for await...ofの中でcursor()を作って回す
for await (const doc of model.find().cursor()) { }
mongo query cursorはfor awaitで回せる
4. cursor無しのfor await...of
for await (const doc of model.find()) { }
mongoose 6以降ではcursor()呼び出さずにqueryを直接for await...ofで回せる
結論
1は現代のnode.js環境では使う必要ないが、とりわけ問題があるわけでもない
cursor回している途中で例外が起きた場合に、プロセスごと終了していいなら、4でいい
だいたいのbatch処理はこれshokai.icon
絶対に2、もしくは1を使うべきシチュエーションがある
cursorで回している処理の中で例外が起きてもプロセス終了できない場合
cursorを回している途中でreturnする可能性がある場合
これらは自前でcursor.close()する必要がある
つまりだいたいのwebサーバーでは2を使うべきshokai.icon
3は、問題があるわけではないが、4で書いたほうがシンプルになる
結局のところ全く同じ処理が行われる
以下で理由を説明していきますshokai.icon
調査ログ
20251112-234315.txt with Codex CLI.icon
必要なライブラリ全てローカルに集めて、Codex CLIにコード追わせるととマジでちゃんと読んで解説してくれるshokai.icon #Codex_CLIの感想
事前学習された知識ではなく、論拠としてコードが出てきて、人間が確認できる
これを定額プランでいつでも回せるのはすごすぎる
mongo query cursorはfor awaitで回せるで使っている.cursor()と、mongoose 6以降ではcursor()呼び出さずにqueryを直接for await...ofで回せるは、実体は同じである
.cursor()なしで回す場合
lib/query.jsがlib/cursor/queryCursor.jsを返している
https://github.com/Automattic/mongoose/blob/51dd78339f8b7b6c772c8587573957fabb5ff1f3/lib/query.js#L5483-L5489
.cursor()で返ってくるのはlib/cursor/queryCursor.js
https://github.com/Automattic/mongoose/blob/51dd78339f8b7b6c772c8587573957fabb5ff1f3/lib/cursor/queryCursor.js#L415-L421
これがasync iteratorなので、for await...ofで回せる
mongooseのlib/cursor/queryCursor.jsはmongodb npmのcursorをそのまま返しているわけではなく、薄いラッパーである
アプリケーション側からfor awaitやnext()で呼び出すと、mongooseがさらにctx.cursor.next()を呼び出す
https://github.com/Automattic/mongoose/blob/51dd78339f8b7b6c772c8587573957fabb5ff1f3/lib/cursor/queryCursor.js#L491-L523
mongodb npmのcursorがそのままアプリケーション側まで露出しているわけではない
for await...of + cursor無しの書き方は、cursor.close()しなくてもリソースリークしないと思っていたが、そんな事はなかったshokai.icon
MongoDBサーバー側
cursorは一定時間でタイムアウトし、killする
TCP接続が切断すると、それに紐づいたcursorを即座にkillする
noCursorTimeoutつきのcursorは時間経過でタイムアウトしないが、TCP接続が切断するとkillされる
Node.jsアプリケーション側
上でまとめている通り、cursorは最後まで回さないとcloseされない
cursor回している途中で処理が打ち切られる可能性があり、しかしプロセスは終了せず動き続けるのであれば、try ~ finally等でcursor.close()する必要がある
cursorを最後まで回しきれば自動的にcloseされる機能が、mongodb npmに実装されている
コンストラクタで、abortListenerにthis.close()を登録している
https://github.com/mongodb/node-mongodb-native/blob/3cf02a8d93d6a8286c39be5a081c6d648408a67e/src/cursor/abstract_cursor.ts#L359-L362
async iteratorの実装。回していて、endであればabortListenerを呼び出すようになっている
https://github.com/mongodb/node-mongodb-native/blob/3cf02a8d93d6a8286c39be5a081c6d648408a67e/src/cursor/abstract_cursor.ts#L471-L526
cursorを最後まで回しきれなかった場合、closeされない
そういう実装がmongodb npmの中に無い
回している途中でエラーが発生したり、returnしたりするとcloseされないという事ですshokai.icon
finallyでcursor.close()するとよい
code:js
let cursor;
try {
cursor = model.find().cursor();
for await (const doc of cursor) {
// cursor回してdocuemntを処理
}
} catch (err) {
// 例外処理
} finally {
await cursor?.close(); // cursorが存在しない可能性もある
}
実装例
Helpfeel専用Cosense Export APIはMongoDB query cursorを閉じ忘れている
business project用監査ログの出力中にクライアント側からstreamが閉じられた場合に、MongoDB query cursorがcloseできていなかった #7939
さらに深く
MongoDBのCursor not foundエラーやnoCursorTimeoutオプションとの向き合い方
MongoDBのcursorで出てくるdocumentは、読み出し時点で古い内容になっている可能性がある
#MongoDB