-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(server): client pause work while blocking commands run #2584
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1327,7 +1327,9 @@ size_t Service::DispatchManyCommands(absl::Span<CmdArgList> args_list, | |
// paired with shardlocal eval | ||
const bool is_eval = CO::IsEvalKind(ArgS(args, 0)); | ||
|
||
if (!is_multi && !is_eval && cid != nullptr) { | ||
const bool is_blocking = cid != nullptr && cid->IsBlocking(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a fix for #2661 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks 🙏🏻 |
||
|
||
if (!is_multi && !is_eval && !is_blocking && cid != nullptr) { | ||
stored_cmds.reserve(args_list.size()); | ||
stored_cmds.emplace_back(cid, tail_args); | ||
continue; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1349,14 +1349,17 @@ OpStatus Transaction::WaitOnWatch(const time_point& tp, WaitKeysProvider wkeys_p | |
auto* stats = ServerState::tl_connection_stats(); | ||
++stats->num_blocked_clients; | ||
DVLOG(1) << "WaitOnWatch wait for " << tp << " " << DebugId(); | ||
|
||
// TBD set connection blocking state | ||
// Wait for the blocking barrier to be closed. | ||
// Note: It might return immediately if another thread already notified us. | ||
cv_status status = blocking_barrier_.Wait(tp); | ||
|
||
DVLOG(1) << "WaitOnWatch done " << int(status) << " " << DebugId(); | ||
--stats->num_blocked_clients; | ||
|
||
// TBD set connection pause state | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, otherwise it's a deadlock 😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also document the reasoning somewhere, it's not evident: Because we set paused atomically and immediately track running commands, it should be correct. If we wake up a thread that wasn't tracked yet, it's also not paused yet, so we'll see the woken up command as running and we'll track it If we wake up a thread that is already paused, it will pause upon unblocking, so no data operations will be performed |
||
ServerState::tlocal()->AwaitPauseState(true); // blocking are always write commands | ||
|
||
OpStatus result = OpStatus::OK; | ||
if (status == cv_status::timeout) { | ||
result = OpStatus::TIMED_OUT; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import random | ||
import string | ||
import pytest | ||
import asyncio | ||
import time | ||
|
@@ -737,3 +738,55 @@ async def do_write(): | |
await asyncio.sleep(0.0) | ||
assert p3.done() | ||
await p3 | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_blocking_command_client_pause(async_client: aioredis.Redis): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍🏻 |
||
""" | ||
1. Check client pause success when blocking transaction is running | ||
2. lpush is paused after running client puase | ||
3. once puased is finished lpush will run and blpop will pop the pushed value | ||
""" | ||
|
||
async def blocking_command(): | ||
res = await async_client.execute_command("blpop key 2") | ||
assert res == ["key", "value"] | ||
|
||
async def lpush_command(): | ||
await async_client.execute_command("lpush key value") | ||
|
||
blocking = asyncio.create_task(blocking_command()) | ||
await asyncio.sleep(0.1) | ||
|
||
res = await async_client.execute_command("client pause 1000") | ||
assert res == "OK" | ||
|
||
lpush = asyncio.create_task(lpush_command()) | ||
assert not lpush.done() | ||
|
||
await lpush | ||
await blocking | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_multiple_blocking_commands_client_pause(async_client: aioredis.Redis): | ||
""" | ||
Check running client pause command simultaneously with running multiple blocking command | ||
from multiple connections | ||
""" | ||
|
||
async def just_blpop(): | ||
key = "".join(random.choices(string.ascii_letters, k=3)) | ||
await async_client.execute_command(f"blpop {key} 2") | ||
|
||
async def client_pause(): | ||
res = await async_client.execute_command("client pause 1000") | ||
assert res == "OK" | ||
|
||
tasks = [just_blpop() for _ in range(20)] | ||
tasks.append(client_pause()) | ||
|
||
all = asyncio.gather(*tasks) | ||
|
||
assert not all.done() | ||
await all |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's already a smart pointer internally, you can just re-assign it