Skip to content

Commit

Permalink
Fix EXEC / DISCARD without MULTI wrongly reset watch (apache#1562)
Browse files Browse the repository at this point in the history
The old code called ResetWatchedKeys before the error, and then
ResetWatchedKeys reset the watched_keys_modified flag which causes
the following inconsistencies (with Redis).

kvrocks output:
```
127.0.0.1:6666> watch a
OK
127.0.0.1:6666> watch b     ->>> other client touch a
OK
127.0.0.1:6666> exec
(error) ERR EXEC without MULTI
127.0.0.1:6666> multi
OK
127.0.0.1:6666(TX)> ping
QUEUED
127.0.0.1:6666(TX)> exec
1) PONG
```

redis output:
```
127.0.0.1:6379> watch a
OK
127.0.0.1:6379> watch b     ->>> other client touch a
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> ping
QUEUED
127.0.0.1:6379(TX)> exec
(nil)
````

Introduced in apache#1279
  • Loading branch information
enjoy-binbin authored and git-hulk committed Jul 30, 2023
1 parent 4c7c75b commit 18321e0
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 4 deletions.
7 changes: 3 additions & 4 deletions src/commands/cmd_txn.cc
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ class CommandMulti : public Commander {
class CommandDiscard : public Commander {
public:
Status Execute(Server *svr, Connection *conn, std::string *output) override {
auto reset_watch = MakeScopeExit([svr, conn] { svr->ResetWatchedKeys(conn); });

if (!conn->IsFlagEnabled(Connection::kMultiExec)) {
*output = redis::Error("ERR DISCARD without MULTI");
return Status::OK();
}

auto reset_watch = MakeScopeExit([svr, conn] { svr->ResetWatchedKeys(conn); });
conn->ResetMultiExec();

*output = redis::SimpleString("OK");

return Status::OK();
Expand All @@ -62,13 +62,12 @@ class CommandDiscard : public Commander {
class CommandExec : public Commander {
public:
Status Execute(Server *svr, Connection *conn, std::string *output) override {
auto reset_watch = MakeScopeExit([svr, conn] { svr->ResetWatchedKeys(conn); });

if (!conn->IsFlagEnabled(Connection::kMultiExec)) {
*output = redis::Error("ERR EXEC without MULTI");
return Status::OK();
}

auto reset_watch = MakeScopeExit([svr, conn] { svr->ResetWatchedKeys(conn); });
auto reset_multiexec = MakeScopeExit([conn] { conn->ResetMultiExec(); });

if (conn->IsMultiError()) {
Expand Down
32 changes: 32 additions & 0 deletions tests/gocase/unit/multi/multi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,22 @@ func TestMulti(t *testing.T) {
require.NoError(t, rdb.Do(ctx, "EXEC").Err())
})

t.Run("EXEC without MULTI is not allowed", func(t *testing.T) {
require.EqualError(t, rdb.Do(ctx, "EXEC").Err(), "ERR EXEC without MULTI")
})

t.Run("EXEC without MULTI should not reset watch", func(t *testing.T) {
rdb2 := srv.NewClient()
defer func() { require.NoError(t, rdb2.Close()) }()

require.NoError(t, rdb.Do(ctx, "WATCH", "x").Err())
require.NoError(t, rdb2.Do(ctx, "SET", "x", "xxx").Err())
require.EqualError(t, rdb.Do(ctx, "EXEC").Err(), "ERR EXEC without MULTI")
require.NoError(t, rdb.Do(ctx, "MULTI").Err())
require.NoError(t, rdb.Do(ctx, "PING").Err())
require.Equal(t, rdb.Do(ctx, "EXEC").Val(), nil)
})

t.Run("EXEC works on WATCHed key not modified", func(t *testing.T) {
require.NoError(t, rdb.Do(ctx, "WATCH", "x", "y", "z").Err())
require.NoError(t, rdb.Do(ctx, "WATCH", "k").Err())
Expand Down Expand Up @@ -268,6 +284,22 @@ func TestMulti(t *testing.T) {
require.Equal(t, rdb.Do(ctx, "EXEC").Val(), nil)
})

t.Run("DISCARD without MULTI is not allowed", func(t *testing.T) {
require.EqualError(t, rdb.Do(ctx, "DISCARD").Err(), "ERR DISCARD without MULTI")
})

t.Run("DISCARD without MULTI should not reset watch", func(t *testing.T) {
rdb2 := srv.NewClient()
defer func() { require.NoError(t, rdb2.Close()) }()

require.NoError(t, rdb.Do(ctx, "WATCH", "x").Err())
require.NoError(t, rdb2.Do(ctx, "SET", "x", "xxx").Err())
require.EqualError(t, rdb.Do(ctx, "DISCARD").Err(), "ERR DISCARD without MULTI")
require.NoError(t, rdb.Do(ctx, "MULTI").Err())
require.NoError(t, rdb.Do(ctx, "PING").Err())
require.Equal(t, rdb.Do(ctx, "EXEC").Val(), nil)
})

t.Run("DISCARD should clear the WATCH dirty flag on the client", func(t *testing.T) {
require.NoError(t, rdb.Set(ctx, "x", 30, 0).Err())
require.NoError(t, rdb.Do(ctx, "WATCH", "x").Err())
Expand Down

0 comments on commit 18321e0

Please sign in to comment.