MongoDBのCursor not foundエラーやnoCursorTimeoutオプションとの向き合い方
前提知識
getMoreの間隔が10分以上かかるケースとは
cursorで取得したdocumentの処理に時間がかかる
CPU処理やI/O待ち時間が長い
mongooseの場合
10件ずつにする
model.find(condition).batchSize(10)
デフォルトは101件、もしくは合計16MBまで、の小さい方が選択される
1回の取得量が減ると
アプリケーション側のメモリ上に乗るdocument数が減る
前提:cursorのcloseは失敗する可能性がある
noCursorTimeoutを使っていると、MongoDBサーバー側にcursorが残ってしまう noCursorTimeout: trueがついていても
TCPの接続が切断されれば、紐づいたcursorは即座にkillされる
batch処理であれば、プロセス終了でkillされるが
webサーバーはプロセス終了しない
TCPの接続が保たれ、長時間MongoDBサーバー内にcursorが残り続ける(=メモリリーク) noCursorTimeout以外の方法を検討しよう
すごくがんばってプログラム書いても、ちょっとしたミスでcloseできない経路ができる事はあるだろうし
そもそもwebサーバーのようなrequest/responseするシステムであれば、batchSizeを指定してgetMore間隔を調整する所からやった方が、全体の処理効率は良くなるはずだ
もちろん、そんな長い処理はwebサーバーでやるな、batch処理でやれというのもある
TCPコネクションはわりとよく切断している
failover、Atlas名物の定期的なprimary/secondar切り替え(failover test)、clusterの構成変更など TCP切断したらcursorがkillされる、の実装を読む by Codex CLI.icon
find/aggregate/count 等のコマンドは実行直後に CommandHelpers::handleMarkKillOnClientDisconnect(opCtx) を呼び出して、クライアント切断時にその OperationContext を強制終了してよいとマークしています
handleMarkKillOnClientDisconnect() は OperationContext::markKillOnClientDisconnect() を叩き、_markKillOnClientDisconnect フラグを立てたうえで、現在の transport session を監視するネットワーク Baton に「このソケットで POLLRDHUP(TCP 切断)が来たら opCtx を kill せよ」と登録します
実際の監視ロジックは ASIO Baton で、AsioNetworkingBaton::markKillOnClientDisconnect() が session に POLLRDHUP ウォッチを付け、切断検知時に _opCtx->markKilled(code) を呼び出します
markKilled() により、その OperationContext で実行中だった getMore などの処理は ErrorCodes::ClientDisconnect で中断されます。
もしその時点でサーバー側 ClientCursor がオペレーションに “pin” されていれば、CursorManager::_killCursor() が _operationUsingCursor(実行中の opCtx)を killOperation してカーソルを killPending にし、後段でderegisterAndDestroyCursor() が走るため、最終的にカーソルは破棄されます
以上が「TCP 接続(transport session)が落ちた瞬間に、実行中のカーソル操作がサーバー側で kill される」ためのコードパスです
cursor消滅前提で、レジューム・リトライして解決できる機構を用意しておくべきshokai.icon
どこまで処理していたか_idをメモしておく
次回のbatch実行で修正されるので気にしない方針とする
未完了フラグを立てておく
など