RedisServiceImpl::Publish creates a transaction via NewTxm() but the engine handler for PublishTxRequest only finishes the request result — it never transitions the txm to Finished. Since an external txm is returned to the free list only when Forward() reports Finished, the publish txm is leaked on every PUBLISH.
Evidence
src/redis_service.cpp:6451-6461:
int RedisServiceImpl::Publish(std::string_view chan, std::string_view msg) {
TransactionExecution *txm = NewTxm(IsolationLevel::ReadCommitted, CcProtocol::OccRead);
PublishTxRequest req(chan, msg, txm);
SendTxRequestAndWaitResult(txm, &req, nullptr); // success path never commits/aborts
return eloqkv_pub_sub_mgr.Publish(chan, msg); // txm pointer dropped here
}
Engine handler data_substrate/tx_service/src/tx_execution.cpp:1074-1084 (ProcessTxRequest(PublishTxRequest)):
for (uint32_t ng_id : *all_node_groups) cc_handler_->PublishMessage(...);
req.tx_result_.Finish(Void{}); // finishes the RESULT, not the txm; no commit, no status=Finished
Forward() (tx_execution.cpp:509-516) returns Idle (not Finished) since tx_status_ was never set to Finished; external txms are reclaimed only via RemoveExternActiveTxm, called only on Finished (tx_service.h:702, 797). SendTxRequestAndWaitResult commits/aborts only on the TX_INIT_FAIL error path. Every other NewTxm call site in the codebase pairs with CommitTx/AbortTx; PUBLISH is the only one that does not.
Impact
One TransactionExecution (a large object) leaks per PUBLISH → unbounded memory growth on pub/sub workloads (keyspace notifications make this hot). Also ext_active_tx_cnt_ never returns to zero, so AllTxFinished() never holds — can block graceful shutdown.
Fix: commit/abort the txm after the publish (or give PublishTxRequest a handler that drives the txm to Finished).
Found during a code audit (docs PR #492). Verified full path against source.
🤖 Found with Claude Code
RedisServiceImpl::Publishcreates a transaction viaNewTxm()but the engine handler forPublishTxRequestonly finishes the request result — it never transitions the txm toFinished. Since an external txm is returned to the free list only whenForward()reportsFinished, the publish txm is leaked on every PUBLISH.Evidence
src/redis_service.cpp:6451-6461:Engine handler
data_substrate/tx_service/src/tx_execution.cpp:1074-1084(ProcessTxRequest(PublishTxRequest)):Forward()(tx_execution.cpp:509-516) returnsIdle(notFinished) sincetx_status_was never set toFinished; external txms are reclaimed only viaRemoveExternActiveTxm, called only onFinished(tx_service.h:702, 797).SendTxRequestAndWaitResultcommits/aborts only on theTX_INIT_FAILerror path. Every otherNewTxmcall site in the codebase pairs withCommitTx/AbortTx; PUBLISH is the only one that does not.Impact
One
TransactionExecution(a large object) leaks per PUBLISH → unbounded memory growth on pub/sub workloads (keyspace notifications make this hot). Alsoext_active_tx_cnt_never returns to zero, soAllTxFinished()never holds — can block graceful shutdown.Fix: commit/abort the txm after the publish (or give
PublishTxRequesta handler that drives the txm toFinished).Found during a code audit (docs PR #492). Verified full path against source.
🤖 Found with Claude Code