Skip to content

Commit 64b6f9d

Browse files
committed
fix(core): potential data race between blocking command and transaction (#2910)
Currently, the EXEC is an exclusive command and will enter the transaction mode when running, but the blocking command's callback function doesn't have this lock. So the callback might get trapped in the wrong transaction mode and break the write batch. For example, the client A sends the command: `BRPOP list 10` and it will enter the blocking mode due to no data. And the client B uses the transaction to send the command: `MULTI; LPUSH list a; EXEC`. Then the client A's callback will be revoked after the `LPUSH` command is executed, but the EXEC is running. So the callback's operation(removing an element from the list) will be counted in the transaction. It closes #2900. It closes #2766.
1 parent f788c11 commit 64b6f9d

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

src/commands/blocking_commander.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "common/lock_manager.h"
2525
#include "event_util.h"
2626
#include "server/redis_connection.h"
27+
#include "server/server.h"
2728

2829
namespace redis {
2930

@@ -70,6 +71,13 @@ class BlockingCommander : public Commander,
7071
void OnWrite(bufferevent *bev) {
7172
bool done{false};
7273
{
74+
// The blocking command should not be executed when the server is in exclusive state,
75+
// because it might have the data race when the server is in transaction mode and run
76+
// the callback here might cause the current execution also in transaction mode.
77+
//
78+
// For more context, please refer to: https://github.com/apache/kvrocks/issues/2900
79+
auto concurrency = conn_->GetServer()->WorkConcurrencyGuard();
80+
7381
auto guard = GetLocks();
7482
done = OnBlockingWrite();
7583
}
@@ -125,7 +133,7 @@ class BlockingCommander : public Commander,
125133
UnblockKeys();
126134
auto bev = conn_->GetBufferEvent();
127135
conn_->SetCB(bev);
128-
bufferevent_enable(bev, EV_READ);
136+
bufferevent_enable(bev, EV_READ | EV_WRITE);
129137
}
130138

131139
protected:

tests/gocase/unit/type/list/list_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ package list
2121

2222
import (
2323
"context"
24+
"errors"
2425
"fmt"
2526
"math/rand"
2627
"strconv"
2728
"strings"
29+
"sync"
2830
"testing"
2931
"time"
3032

@@ -1513,3 +1515,47 @@ func testList(t *testing.T, configs util.KvrocksServerConfigs) {
15131515
})
15141516
}
15151517
}
1518+
1519+
// TestPotentialDataRaceInBlockingCommand is to test blocking command's callback
1520+
// shouldn't have data race with concurrent transaction behavior.
1521+
//
1522+
// For more information, please refer to: https://github.com/apache/kvrocks/issues/2900
1523+
func TestPotentialDataRaceInBlockingCommand(t *testing.T) {
1524+
srv := util.StartServer(t, map[string]string{})
1525+
defer srv.Close()
1526+
1527+
ctx, cancelFn := context.WithCancel(context.Background())
1528+
rdb := srv.NewClient()
1529+
defer func() { require.NoError(t, rdb.Close()) }()
1530+
1531+
listKey := "mylist"
1532+
rdb.Del(ctx, listKey)
1533+
var wg sync.WaitGroup
1534+
for i := 0; i < 4; i++ {
1535+
wg.Add(1)
1536+
go func() {
1537+
defer wg.Done()
1538+
for {
1539+
err := rdb.BLPop(ctx, 3500*time.Millisecond, listKey).Err()
1540+
if errors.Is(err, redis.Nil) {
1541+
continue
1542+
} else if errors.Is(err, context.Canceled) {
1543+
return
1544+
} else {
1545+
require.NoError(t, err)
1546+
}
1547+
}
1548+
}()
1549+
}
1550+
1551+
for i := 0; i < 64; i++ {
1552+
pipe := rdb.TxPipeline()
1553+
pipe.LPush(ctx, listKey, "element")
1554+
_, err := pipe.Exec(ctx)
1555+
require.NoError(t, err)
1556+
time.Sleep(time.Millisecond * 100)
1557+
}
1558+
1559+
cancelFn()
1560+
wg.Wait()
1561+
}

0 commit comments

Comments
 (0)