MongoDBのcursorで出てくるdocumentは、読み出し時点で古い内容になっている可能性がある
前提知識
mongooseのfind query cursorは色々な回し方があるけどどれ使えばいいの?
_idを使ってもう一度読み出すようにした方がいい場合があるshokai.icon
code:js
let cursor
try {
cursor = model.find(condition).cursor();
for await (const { _id } of cursor) {
const doc = await model.findOne({ _id }); // _idを使ってもう一度読み出す
}
} catch (err) {
// 例外処理
} finally {
cursor?.close();
}
理由
batch処理などでMongoDB query cursorで時間をかけて処理している場合に、他のプロセスがdocumentを更新する可能性があるから
MongoDBサーバー側のcursorの挙動
cursor作成時点で条件にマッチしていたdocumentだけが返る
途中で他のプロセスがdocumentを変更し、条件にマッチするdocumentが新たに発生した場合
今回のcursorが返すdocumentには含まれない
途中で他のプロセスがdocumentを変更し、マッチしていたdocumentが条件から外れてしまった場合
これはタイミングによる
MongoDBサーバーがBSONをfetchし、アプリケーションに返す処理がある
BSONをfetchした後、他のプロセスが変更した場合
BSON fetch時点の内容のdocumentがアプリケーションに返る
つまり古いdocumentが返るshokai.icon
BSONをまだfetchしていない位置にあるdocumentが変更された場合
それによって条件にマッチしなくなった場合
documentはアプリケーションに送信されない
BSON fetch時点でもう一度条件チェックが行われるため
変更後も条件にマッチする場合
documentはアプリケーションに送信される(当然)
つまり新しいdocumentが返るshokai.icon
まとめ
新しいdocumentが返る事もあるし、古いdocumentが返る事もある
少なくとも、アプリケーション側に現れるdocumentは、find queryの条件にはマッチする
しかしMongoDBのストレージ内の最新データとは食い違っている可能性がある
どうするか
cursor回してるループの中で、_idを使ってもう一度最新のdocumentを読み直すのが安全
cursorの実装を読む by Codex CLI.icon
src/mongo/db/exec/classic/collection_scan.cpp:402-407 で、CollectionScan がレコードを取り出すたびに WorkingSetMember に recordId と BSON のコピーを格納し、その際に現在の RecoveryUnit から取った SnapshotId を resetDocument() で貼り付けています。ここで BSON は record->data.releaseToBson() の結果(=その時点のドキュメント)を保持するので、以後このエ ントリがユーザーに返るときには取得時点の内容のままです。
WorkingSetMember 自体の実装 (src/mongo/db/exec/classic/working_set.cpp:120-147, 189-194) は doc に Snapshotted<Document> を使い、makeObjOwnedIfNeeded() で BSON を完全コピー (owned)にしてから外部へ返すようになっています。これにより、後からストレージ上の同じ RecordId が更新されても、カーソルが既に保持している WorkingSetMember の内容は書き換わりません。
まだ BSON をフェッチしていない(RID だけを持っている)ケースでは、実際に getMore などで参照するときに WorkingSetCommon::fetch() が呼ばれます (src/mongo/db/exec/classic/working_set_common.cpp:101-218)。ここでは
現在のスナップショット (shard_role_details::getRecoveryUnit(opCtx)->getSnapshotId()) を取り直し、
resetDocument() でその時点の BSON を再取得、
さらに、保持している index key が今でもそのドキュメントに対応しているかを検証し、合わなければ false を返してそのエントリを捨てる
という流れです。つまり途中でドキュメントが条件から外れるように更新されていた場合はこの検証で弾かれ、カーソルからは出てこなくなります。
アプリケーション側でfind(condition).batchSize(1)を指定したらどうなるか?
答え:変わらない
Query.prototype.batchSizeはMongoDBサーバーからアプリケーションに返却されるdocumentの数を制御するだけ
MongoDBサーバー内で、BSONを一度にfetchする件数を制御するパラメータではない