WATCH key; MULTI; EXEC with no queued commands leaks the RepeatableRead watch transaction: MultiExec early-returns on an empty command list without committing or aborting the txm, and the handler then nulls txm_ so its destructor's abort is skipped. The txm and its read locks leak.
Evidence
WatchKeys assigns the watch txm (src/redis_handler.cpp:1877-1884):
if (txm_ == nullptr)
txm_ = redis_impl_->NewTxm(txn_isolation_level_, txn_protocol_); // RepeatableRead + OCC
MultiExec empty-list early return (src/redis_service.cpp:2066-2069):
if (cmd_reqs.empty()) { redis_reply.OnNil(); return NO_ERROR; } // txm untouched
MultiTransactionHandler::Run nulls txm_ after the call (redis_handler.cpp:1829), so the destructor's if (txm_) AbortTx(txm_) (:1680) is a no-op → the watch txm is never aborted, leaking it (and any read locks/intents it holds) from the pool.
Repro
WATCH k then MULTI then EXEC (empty queue) → leaks one txm; also replies nil whereas Redis replies an empty array (empty array).
Fix: in the empty-EXEC path, abort (or commit) the passed-in txm before returning; return an empty array to match Redis.
Found during a code audit (docs PR #492). Verified against source at the cited lines.
🤖 Found with Claude Code
WATCH key; MULTI; EXECwith no queued commands leaks the RepeatableRead watch transaction:MultiExecearly-returns on an empty command list without committing or aborting the txm, and the handler then nullstxm_so its destructor's abort is skipped. The txm and its read locks leak.Evidence
WatchKeysassigns the watch txm (src/redis_handler.cpp:1877-1884):MultiExecempty-list early return (src/redis_service.cpp:2066-2069):MultiTransactionHandler::Runnullstxm_after the call (redis_handler.cpp:1829), so the destructor'sif (txm_) AbortTx(txm_)(:1680) is a no-op → the watch txm is never aborted, leaking it (and any read locks/intents it holds) from the pool.Repro
WATCH kthenMULTIthenEXEC(empty queue) → leaks one txm; also repliesnilwhereas Redis replies an empty array(empty array).Fix: in the empty-EXEC path, abort (or commit) the passed-in txm before returning; return an empty array to match Redis.
Found during a code audit (docs PR #492). Verified against source at the cited lines.
🤖 Found with Claude Code