Skip to content

Don't re-run a blocked client's pending command in handleReadJobs() - #4370

Open
dgershko wants to merge 1 commit into
valkey-io:unstablefrom
dgershko:fix-blocked-client-reexec-io-threads
Open

Don't re-run a blocked client's pending command in handleReadJobs()#4370
dgershko wants to merge 1 commit into
valkey-io:unstablefrom
dgershko:fix-blocked-client-reexec-io-threads

Conversation

@dgershko

@dgershko dgershko commented Aug 9, 2026

Copy link
Copy Markdown

blockForKeys() sets pending_command so a blocking command is re-executed once the client is unblocked. handleReadJobs() calls processPendingCommandAndInputBuffer() after processClientsCommandsBatch(), which re-enters processCommand() on the client it just blocked, blocking it a second time.

With io-threads active a single in-flight BLPOP reports blocked_clients:2, and the counter leaks one reference per blocking command:

valkey-server --io-threads 2 --io-threads-always-active yes
# after 5 BLPOP timeouts, all those clients since disconnected:
blocked_clients:5

Reproduction:
./runtest --single unit/type/list --io-threads --only "Unblock fairness is kept while pipelining"

Existing coverage only fails under --io-threads, which CI never passes; the new test in unit/io-threads.tcl pins
io-threads itself so a default run covers this path.

Guard the call on the states that mean the client already owns an unprocessed command, matching processUnblockedClients(). Guarding the call rather than the loop iteration keeps blocked clients reaching connUpdateState().

blockForKeys() sets pending_command so a blocking command is re-executed
once the client is unblocked. handleReadJobs() calls
processPendingCommandAndInputBuffer() after processClientsCommandsBatch(),
which re-enters processCommand() on the client it just blocked, blocking it
a second time.

With io-threads active a single in-flight BLPOP reports blocked_clients:2,
and the counter leaks one reference per blocking command:

    valkey-server --io-threads 2 --io-threads-always-active yes
    # after 5 BLPOP timeouts, all those clients since disconnected:
    blocked_clients:5

Guard the call on the states that mean the client already owns an
unprocessed command, matching processUnblockedClients(). Guarding the call
rather than the loop iteration keeps blocked clients reaching
connUpdateState().

Signed-off-by: Daniel Gershkovich <dgershko@gmail.com>
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9bc8545b-71fd-432c-abd9-eaa8ca8232ad

📥 Commits

Reviewing files that changed from the base of the PR and between 2d69e5e and ae56c2c.

📒 Files selected for processing (2)
  • src/io_threads.c
  • tests/unit/io-threads.tcl

📝 Walkthrough

Walkthrough

The IO-thread read completion path now skips blocked, unblocked, and immediately closing clients. Regression tests cover deferred BLPOP wakeup and timeout cleanup.

Changes

IO-thread blocking commands

Layer / File(s) Summary
Filter blocked clients during read completion
src/io_threads.c
Read completion handling skips pending command processing and beforeNextClient for blocked, unblocked, or immediately closing clients.
Validate deferred and timed-out BLPOP
tests/unit/io-threads.tcl
Tests verify blocked-client registration, wakeup cleanup after LPUSH, timeout cleanup, and server responsiveness.

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

Possibly related PRs

Suggested reviewers: madolson

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main fix: preventing blocked clients from re-running pending commands in handleReadJobs().
Description check ✅ Passed The description explains the duplicate blocking issue, its impact, reproduction steps, and the regression test added by the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@dgershko
dgershko marked this pull request as ready for review August 9, 2026 13:14

@ranshid ranshid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice catch @dgershko !

Comment thread src/io_threads.c
Comment on lines +868 to +870
if (!c->flag.blocked && !c->flag.unblocked && !c->flag.close_asap &&
processPendingCommandAndInputBuffer(c) == C_OK)
beforeNextClient(c);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@valkey-review-bot can you review this PR, this also adds 2 more checks which might be more defensive rather then targeting to solve a real bug (!c->flag.unblocked && !c->flag.close_asap) - can you please verify that and if not please suggest extending the tests

@valkey-review-bot

valkey-review-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

The blocked check is the bug fix. The other two checks also protect real states rather than serving only as defensive assertions: unblocked means the client is already queued for processUnblockedClients(), and close_asap means it is queued for asynchronous free. The current BLPOP tests exercise only blocked; please add targeted coverage for those two branches.

Comment thread src/io_threads.c
if (!c || !c->conn) continue;

if (processPendingCommandAndInputBuffer(c) == C_OK) beforeNextClient(c);
if (!c->flag.blocked && !c->flag.unblocked && !c->flag.close_asap &&

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.

unblocked and close_asap are valid guards, but neither is covered by the new BLPOP tests. queueClientForReprocessing() sets unblocked specifically so processUnblockedClients() owns the pending command (src/blocked.c:206-212), while freeClientAsync() sets close_asap before the client is actually freed (src/networking.c:2271-2275); running this drain in either state would bypass that ownership/teardown. Extend the IO-thread tests with a CLIENT PAUSE/CLIENT UNPAUSE case for unblocked, and a completion whose client is killed or hits a query/output limit for close_asap, then verify no queued command executes and the client is removed.

@ranshid ranshid added the run-extra-tests Run extra tests on this PR (Runs all tests from daily except valgrind and RESP) label Aug 9, 2026
@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.00%. Comparing base (2d69e5e) to head (ae56c2c).

Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #4370      +/-   ##
============================================
+ Coverage     76.98%   77.00%   +0.01%     
============================================
  Files           162      162              
  Lines         81786    81788       +2     
============================================
+ Hits          62965    62982      +17     
+ Misses        18821    18806      -15     
Files with missing lines Coverage Δ
src/io_threads.c 76.80% <100.00%> (+0.09%) ⬆️

... and 19 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-extra-tests Run extra tests on this PR (Runs all tests from daily except valgrind and RESP)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants