Skip to content

fix: PUBLISH leaks a TransactionExecution on every call - #527

Merged
liunyl merged 4 commits into
mainfrom
claude/codex-gated-development-424f01
Jul 11, 2026
Merged

fix: PUBLISH leaks a TransactionExecution on every call#527
liunyl merged 4 commits into
mainfrom
claude/codex-gated-development-424f01

Conversation

@liunyl

@liunyl liunyl commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #505

Problem

RedisServiceImpl::Publish creates an external txm to carry the cross-node PublishTxRequest, but no path ever commits or aborts it. The engine handler (ProcessTxRequest(PublishTxRequest&)) finishes only the request result — it never drives the txm to Finished — and an external txm is returned to the free list only via RemoveExternActiveTxm, which fires only when Forward() reports Finished. SendTxRequestAndWaitResult's internal abort covers only ObjectCommandTxRequest/MultiObjectCommandTxRequest with auto_commit_, which PublishTxRequest is not.

Result: one TransactionExecution leaked per PUBLISH (including Lua redis.call('publish', ...)), ext_active_tx_cnt_ grows monotonically, and AllTxFinished() never holds again — the concrete consumer is node-group leader transfer, which spins in while (!AllTxFinished()) (cc_node_service.cpp:2502).

Fix

Release the txm with AbortTx(txm) after the request wait — the caller-owns-lifecycle pattern every other NewTxm site in this layer uses (e.g. the FaultInject path). The publish txm has an empty read/write set, so the abort takes the empty-set fast path of TransactionExecution::CommitTx (tx_execution.cpp:574-587): an asynchronous enqueue + enlist, no blocking work added to the hot path. The call is safe unconditionally: the FIFO request queue orders it after the in-flight publish request, PublishTxRequest matches neither dynamic_cast in the TX_INIT_FAIL path (no double abort), and aborting an init-failed txm takes the established tx_term_ < 0 branch.

docs/04-scripting-pubsub-blocking.md is updated in the same change per the docs maintenance rule (PUBLISH-flow prose + line-reference refreshes in the Pub/Sub section).

Verification (Debug build, ELOQDSS_ROCKSDB, single node)

Leak oracle: gdb probe summing ext_active_tx_cnt_ + active_tx_cnt_ across TxProcessors.

pre-fix post-fix
idle TOTAL_EXT=4 TOTAL_EXT=0
right after redis-cli -r 10000 publish c m 10004 0
after 15s drain window 10004 (no drain) 0
after +100k benchmark publishes 110004 0
redis-benchmark -n 100000 -c 10 publish c m 41,964 req/s, avg 0.232 ms 73,206 req/s, avg 0.117 ms
VmRSS under that load 242 MB 104 MB

Throughput improves ~74% because txms now recycle through the free list instead of allocating a fresh TransactionExecution per publish.

Functional: SUBSCRIBE/PUBLISH delivery + reply count, Lua EVAL redis.call('publish'), and the unit/eloq/flaky_test/pubsub, unit/eloq/multi, unit/eloq/string TCL suites all pass.

Notes

  • CI currently runs no pubsub TCL test (tests/unit/eloq/*.tcl has none; pubsub.tcl is quarantined under flaky_test/), so this ships without an automated leak regression test — there is also no protocol-surface observable for active txms. Suggested follow-ups: promote a deterministic pubsub smoke test into CI, and expose an active-transaction gauge (e.g. TxService::AllTxFinished()) via INFO/metrics.
  • A theoretical engine-level race between the TX_INIT_FAIL drain loop and a concurrently enqueued abort exists for all caller-side AbortTx-after-init-failure patterns (pre-existing, nanosecond window, not a regression of this change).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability of Pub/Sub publishing across clustered nodes.
    • Ensured publishing transactions are properly released after cross-node operations.
  • Tests

    • Added coverage for publishing with no subscribers.
    • Added coverage for subscriber counts returned from Lua scripts.
    • Reduced Pub/Sub test flakiness by improving unsubscribe and confirmation handling.
  • Documentation

    • Clarified Pub/Sub locking, ownership, and cross-node publishing behavior.

RedisServiceImpl::Publish created a TransactionExecution for the
cross-node PublishTxRequest but never committed or aborted it. The
engine handler only finishes the request result, so the txm never
reached Finished and was never returned to the free list: one txm
leaked per PUBLISH, ext_active_tx_cnt_ grew monotonically, and
AllTxFinished() could never hold again (blocking graceful node-group
leader transfer).

Release the txm with AbortTx after the request completes. The publish
txm has an empty read/write set, so the abort takes the empty-set fast
path: an asynchronous enqueue+enlist that adds no blocking work to the
hot path.

Verified with a Debug build and a gdb probe over the TxProcessor
counters: pre-fix, 10k PUBLISHes grow TOTAL_EXT from 4 to 10004 with
zero drain; post-fix it stays at 0 immediately. PUBLISH throughput
improves 42k -> 73k req/s (txms now recycle instead of allocating
fresh ones), and the pubsub/multi/string TCL suites pass.

Fixes #505

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f67469c4-ffe7-4b34-982f-a6023346adc3

📥 Commits

Reviewing files that changed from the base of the PR and between e5ca361 and 1d7bfae.

📒 Files selected for processing (1)
  • tests/unit/eloq/pubsub.tcl

Walkthrough

PUBLISH now explicitly aborts its short-lived remote transaction. Pub/Sub documentation reflects this lifecycle, and tests improve unsubscribe synchronization while adding coverage for zero-subscriber and Lua-triggered publishing.

Changes

PUBLISH transaction cleanup

Layer / File(s) Summary
Abort remote publish transaction
src/redis_service.cpp, docs/04-scripting-pubsub-blocking.md
Remote PUBLISH transactions are explicitly aborted, and the documented cross-node flow describes asynchronous enlistment and cleanup.
Validate PUBLISH behavior
tests/unit/eloq/pubsub.tcl
Tests verify that publishing without subscribers returns 0 and that Lua-triggered publishing returns the subscriber count and delivers the message.
Synchronize unsubscribe assertions
tests/unit/eloq/pubsub.tcl
Tests unsubscribe or punsubscribe before closing clients and consume all confirmation messages before later publish checks.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

I’m a bunny guarding channels bright,
Transactions hop back home tonight.
Unsubscribe bells ring clear,
Lua sends messages far and near,
Zero listeners? Zero cheer! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing the PUBLISH TransactionExecution leak.
Linked Issues check ✅ Passed The code change aborts the publish txm after the request completes, which addresses the leak described in #505.
Out of Scope Changes check ✅ Passed The docs and Pub/Sub test updates are directly related to the PUBLISH leak fix and its verification.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/codex-gated-development-424f01

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

liunyl and others added 3 commits July 11, 2026 02:45
tests/unit/eloq/flaky_test/pubsub.tcl runs in CI but its failures are
swallowed, so no gating test exercises PUBLISH at all - which is how
the txm leak (#505) shipped unnoticed. Add a deterministic subset
directly under tests/unit/eloq/ where failures fail CI: publish with
no subscribers, SUBSCRIBE/PSUBSCRIBE delivery + reply counts, and
publish via Lua EVAL. Every assertion is a synchronous request or a
blocking read of a confirmation the server sends after updating the
registry under pub_sub_mu_, so there are no timing sleeps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The framework's unsubscribe/punsubscribe helpers consume zero
confirmation messages when called without channel arguments. Upstream
Redis is single-threaded so the next command is serialized after the
unsubscribe anyway, but EloqKV processes connections concurrently:
nothing orders the pub/sub registry update before another connection's
PUBLISH, so the publish-returns-0 assertions raced it. The file papered
over this with 'after 100' sleeps, which still flake on slow or loaded
machines - the reason it lives in flaky_test/.

Read all per-channel confirmations instead: EloqKV flushes one per
channel inside the single pub_sub_mu_ critical section that performs
the erasures, and PUBLISH takes the same mutex, so once the last
confirmation is read a subsequent publish deterministically counts 0.

Reproduced and verified on a Debug build: a 300-iteration tight loop of
subscribe -> unsubscribe-all -> publish under concurrent benchmark load
failed every round before this change and passes 3/3 after; the full
file passes 5/5 loaded + 5/5 unloaded rounds. Kept in flaky_test/ for
now so the CI matrix can demonstrate stability before promotion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The unsubscribe-all race fix (previous commit) held up: the file passed
every phase of every CI matrix job non-gating, including the 3-node
cluster log-replay phase on the slowest arm64/ELOQSTORE variant, with
zero swallowed failures, plus loaded/unloaded local stress rounds.

Merge it into tests/unit/eloq/pubsub.tcl where failures fail CI,
absorbing the smoke file's two unique cases (publish with no
subscribers; publish via Lua EVAL), and delete the flaky_test copy.
Also fix the remaining teardown races before gating: three tests closed
deferring clients while still subscribed, and server-side removal
happens asynchronously in the connection destructor, so a later publish
to the same channel could still count them — unsubscribe explicitly
(confirmations consumed) before close.

Merged suite passes 3/3 rounds under concurrent benchmark load and
3/3 unloaded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/eloq/pubsub.tcl`:
- Line 186: Remove the pasted failed-test diagnostic comment near the affected
test in pubsub.tcl; retain the surrounding test logic and assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f67469c4-ffe7-4b34-982f-a6023346adc3

📥 Commits

Reviewing files that changed from the base of the PR and between e5ca361 and 1d7bfae.

📒 Files selected for processing (1)
  • tests/unit/eloq/pubsub.tcl

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/eloq/pubsub.tcl`:
- Line 186: Remove the pasted failed-test diagnostic comment near the affected
test in pubsub.tcl; retain the surrounding test logic and assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f67469c4-ffe7-4b34-982f-a6023346adc3

📥 Commits

Reviewing files that changed from the base of the PR and between e5ca361 and 1d7bfae.

📒 Files selected for processing (1)
  • tests/unit/eloq/pubsub.tcl
🛑 Comments failed to post (1)
tests/unit/eloq/pubsub.tcl (1)

186-186: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove debug artifact comment.

This line appears to be error output from a failed test run pasted as a comment. It references an internal assertion failure in consume_subscribe_messages and adds no documentation value.

🧹 Proposed fix
-
-# Expected 'subscribe' to be equal to 'unknown' (context: type source line 838 file /home/eloq/workspace/eloqkv/tests/support/util.tcl cmd {assert_equal $type [lindex $msg 0]} proc ::consume_subscribe_messages level 2)
 # 'CLIENT REPLY OFF' unsupported
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

# 'CLIENT REPLY OFF' unsupported
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/eloq/pubsub.tcl` at line 186, Remove the pasted failed-test
diagnostic comment near the affected test in pubsub.tcl; retain the surrounding
test logic and assertions unchanged.

@liunyl
liunyl merged commit bdb8057 into main Jul 11, 2026
20 checks passed
@liunyl
liunyl deleted the claude/codex-gated-development-424f01 branch July 11, 2026 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PUBLISH leaks a TransactionExecution on every call (txm never committed/aborted)

2 participants