Background
Found while evaluating where to put the blind_remove ENOTSUP check for WT-18922. WT-18922 keeps that check in __session_open_cursor, so it does not depend on this fix. Related: WT-18941.
Problem
In _wt_cursor_cache_get (src/cursor/cur_std.c), once cursor->reopen() succeeds, the cursor is live again. _wti_cursor_reopen takes a dhandle reference, moves the cursor from its cache bucket onto session->cursors, and clears WT_CURSTD_CACHED. The steps after that use WT_RET:
WT_RET(__cursor_reuse_or_init(session, cursor, cfg, &readonly, NULL, &cdump)); ... WT_RET(__wt_config_gets_def(session, cfg, "read_once", 0, &cval));
If either step fails, the function returns without setting *cursorp and without closing the cursor. The caller (_session_open_cursor, or _wt_open_cursor for layered constituents) is left with a NULL cursor, so its error path closes nothing. A failed reopen, by contrast, is handled: the cursor is closed.
Impact
The cursor stays on the session's open list, out of the application's reach, until the session is closed:
- It holds a dhandle reference, so sweep cannot close the handle, and operations that need exclusive access can be expected to fail with EBUSY.
- It never returns to the cache, so later opens of that URI build a new cursor.
MongoDB keeps sessions open for a long time, so this can last a long time.
Reachability
Not reachable today. The steps after reopen only look up config that was already validated at API entry. Configs that add steps that can fail (dump, bulk, readonly, next_random, statistics, checkpoint) are never served from the cache; see _wti_cursors_can_be_cached. Any new check in _cursor_reuse_or_init that can fail makes this reachable.
Reproduction
Move the blind_remove ENOTSUP check into __cursor_reuse_or_init. The plain-table scenario of test_layered_cursor31.test_cursor_cache then fails. The rejected blind_remove=true open reuses the cached cursor and leaves it open, so the next default open misses the cache: AssertionError: 5 not greater than 5 on stat.conn.cursor_reopen.
Proposed fix
Use WT_ERR for the steps after reopen, and close the cursor on error. The cursor is still cacheable, so closing it puts it back in the cache. That matches what __session_open_cursor's error path does today for a cursor rejected after it has been opened.
return (WT_NOTFOUND); err: WT_TRET(cursor->close(cursor)); return (ret);
With this change, plus the fix for WT-18941, the reproduction above passes under ASan. So do 406 tests across the cursor-cache, dump, sweep and layered test files.
Testing
Nothing a user does can trigger this today. A regression test needs a forced failure after the reopen. It should check that the cursor goes back to the cache (cursor_reopen increases on the next open) and that no open cursor is left behind.