Skip to content

Commit fad0208

Browse files
authored
fix: preserve correct command order in MULTI/EXEC transactions (#5954)
* fix: preserve correct command order in MULTI/EXEC transactions * fix: review comments * fix: broken test
1 parent edf5f25 commit fad0208

File tree

2 files changed

+42
-2
lines changed

2 files changed

+42
-2
lines changed

src/server/main_service.cc

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1620,7 +1620,11 @@ DispatchResult Service::InvokeCmd(const CommandId* cid, CmdArgList tail_args,
16201620
// We are not sending any admin command in the monitor, and we do not want to
16211621
// do any processing if we don't have any waiting connections with monitor
16221622
// enabled on them - see https://redis.io/commands/monitor/
1623-
if (!ServerState::tlocal()->Monitors().Empty() && (cid->opt_mask() & CO::ADMIN) == 0) {
1623+
// For EXEC command specifically, we dispatch monitor after executing all queued commands
1624+
// to preserve correct ordering (MULTI, commands, EXEC) instead of (MULTI, EXEC, commands)
1625+
bool should_dispatch_monitor = !ServerState::tlocal()->Monitors().Empty() &&
1626+
(cid->opt_mask() & CO::ADMIN) == 0 && cid != exec_cid_;
1627+
if (should_dispatch_monitor) {
16241628
DispatchMonitor(cntx, cid, tail_args);
16251629
}
16261630

@@ -2585,6 +2589,13 @@ void Service::Exec(CmdArgList args, const CommandContext& cmd_cntx) {
25852589
}
25862590

25872591
cntx->cid = exec_cid_;
2592+
2593+
// Dispatch EXEC to monitor after all queued commands have been executed
2594+
// to preserve correct ordering (MULTI, commands, EXEC)
2595+
if (!ServerState::tlocal()->Monitors().Empty() && (exec_cid_->opt_mask() & CO::ADMIN) == 0) {
2596+
DispatchMonitor(cntx, exec_cid_, args);
2597+
}
2598+
25882599
VLOG(2) << "Exec completed";
25892600
}
25902601

tests/dragonfly/connection_test.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,36 @@ async def test_monitor_command_multi(async_pool):
120120
expected = CollectedRedisMsg.all_from_src(*expected)
121121

122122
# The order is random due to squashing
123-
assert set(expected) == set(collected[2:])
123+
assert set(expected) == set(collected[1:-1])
124+
125+
126+
"""
127+
Test MONITOR command preserves correct order for MULTI/EXEC sequences
128+
Regression test for https://github.com/dragonflydb/dragonfly/issues/5953
129+
"""
130+
131+
132+
@dfly_args({"proactor_threads": 4})
133+
async def test_monitor_command_multi_exec_order(async_pool):
134+
monitor = CollectingMonitor(aioredis.Redis(connection_pool=async_pool))
135+
await monitor.start()
136+
137+
c = aioredis.Redis(connection_pool=async_pool)
138+
p = c.pipeline(transaction=True)
139+
p.ping()
140+
p.set("key1", "value1")
141+
p.get("key1")
142+
await p.execute()
143+
144+
collected = await monitor.stop()
145+
146+
# Verify the commands appear in the correct order: MULTI, PING, SET, GET, EXEC
147+
assert len(collected) == 5
148+
assert "MULTI" in collected[0].cmd.upper()
149+
assert "PING" in collected[1].cmd.upper()
150+
assert "SET" in collected[2].cmd.upper()
151+
assert "GET" in collected[3].cmd.upper()
152+
assert "EXEC" in collected[4].cmd.upper()
124153

125154

126155
"""

0 commit comments

Comments
 (0)