Skip to content

WATCH then empty EXEC leaks the watch transaction (and returns nil instead of empty array) #506

@liunyl

Description

@liunyl

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions