Skip to content

Commit 31cd4e9

Browse files
committed
feat(script): add SCRIPT KILL and lua-time-limit support
This commit merges all changes of the current dev branch compared to unstable into a single commit. It includes implementation of the lua-time-limit configuration option, SCRIPT KILL command to safely interrupt long-running scripts, connection guard optimizations, worker event loop recursive event handling, and macOS TCP listener socket sharing.
1 parent 82eaa90 commit 31cd4e9

15 files changed

Lines changed: 442 additions & 18 deletions

File tree

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR (CMAKE_CXX_COMPILER_ID STREQUAL "C
254254
endif()
255255
endif()
256256

257+
if (APPLE AND ENABLE_LUAJIT)
258+
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-no_deduplicate")
259+
endif()
260+
257261
# kvrocks objects target
258262
file(GLOB_RECURSE KVROCKS_SRCS src/*.cc)
259263
list(FILTER KVROCKS_SRCS EXCLUDE REGEX src/cli/main.cc)
@@ -286,6 +290,9 @@ target_compile_definitions(kvrocks_objs PUBLIC KVROCKS_STORAGE_ENGINE=RocksDB)
286290
if(ENABLE_OPENSSL)
287291
target_compile_definitions(kvrocks_objs PUBLIC ENABLE_OPENSSL)
288292
endif()
293+
if(ENABLE_LUAJIT)
294+
target_compile_definitions(kvrocks_objs PUBLIC ENABLE_LUAJIT)
295+
endif()
289296
if(ENABLE_NEW_ENCODING)
290297
target_compile_definitions(kvrocks_objs PUBLIC METADATA_ENCODING_VERSION=1)
291298
else()

cmake/luajit.cmake

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,28 @@ FetchContent_GetProperties(luajit)
4747
if (NOT lua_POPULATED)
4848
FetchContent_Populate(luajit)
4949

50-
set(LUA_CFLAGS "-DLUA_ANSI -DENABLE_CJSON_GLOBAL -DREDIS_STATIC= -DLUA_USE_MKSTEMP")
50+
# We use LUAJIT_CFLAGS instead of LUA_CFLAGS and we do not define LUA_ANSI.
51+
# LuaJIT relies heavily on architecture-specific assembly code and compiler optimization settings,
52+
# and LUA_ANSI is meant for standard Lua to restrict to ANSI C which LuaJIT does not support.
53+
set(LUAJIT_CFLAGS "-DENABLE_CJSON_GLOBAL -DREDIS_STATIC= -DLUA_USE_MKSTEMP -DLUAJIT_ENABLE_CHECKHOOK")
54+
set(LUAJIT_LDFLAGS "")
5155
if((CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") OR
5256
(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang"))
53-
set(LUA_CFLAGS "${LUA_CFLAGS} -isysroot ${CMAKE_OSX_SYSROOT}")
57+
set(LUAJIT_CFLAGS "${LUAJIT_CFLAGS} -isysroot ${CMAKE_OSX_SYSROOT}")
58+
set(LUAJIT_LDFLAGS "-isysroot ${CMAKE_OSX_SYSROOT}")
5459
endif ()
5560

5661
if (CMAKE_HOST_APPLE)
5762
set(MACOSX_TARGET "MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}")
5863
endif ()
5964

65+
# We pass C compiler path CC and custom preprocessor definitions via XCFLAGS instead of CFLAGS.
66+
# Overriding CFLAGS on the make command line completely discards LuaJIT's own Makefile CFLAGS (such as
67+
# -O2 and target-specific compiler optimizations), which breaks the JIT compiler and assembler VM
68+
# stack unwinding on platforms like macOS arm64, leading to CPU spin locks or crashes.
6069
add_custom_target(make_luajit
6170
COMMAND ${MAKE_COMMAND} libluajit.a ${NINJA_MAKE_JOBS_FLAG}
62-
"CFLAGS=${LUA_CFLAGS}" ${MACOSX_TARGET}
71+
"CC=${CMAKE_C_COMPILER}" "XCFLAGS=${LUAJIT_CFLAGS}" "LDFLAGS=${LUAJIT_LDFLAGS}" "HOST_LDFLAGS=${LUAJIT_LDFLAGS}" ${MACOSX_TARGET}
6372
COMMAND ${CMAKE_COMMAND} -E make_directory ${luajit_BINARY_DIR}/include
6473
COMMAND sh -c "cp ${luajit_SOURCE_DIR}/src/*.h ${luajit_SOURCE_DIR}/src/*.hpp ${luajit_BINARY_DIR}/include/"
6574
WORKING_DIRECTORY ${luajit_SOURCE_DIR}/src

kvrocks.conf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,13 @@ txn-context-enabled no
492492
# Default: no
493493
lua-strict-key-accessing no
494494

495+
# Max execution time of a Lua script in milliseconds.
496+
# If the maximum execution time is reached, the server will log it
497+
# and start rejecting other queries with a BUSY error.
498+
# Setting it to 0 or a negative value means no timeout.
499+
# Default: 0 (disabled)
500+
lua-time-limit 0
501+
495502
################################## TLS ###################################
496503

497504
# By default, TLS/SSL is disabled, i.e. `tls-port` is set to 0.

src/commands/cmd_script.cc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class CommandScript : public Commander {
6868
// There's a little tricky here since the script command was the write type
6969
// command but some subcommands like `exists` were readonly, so we want to allow
7070
// executing on slave here. Maybe we should find other way to do this.
71-
if (srv->IsSlave() && subcommand_ != "exists") {
71+
if (srv->IsSlave() && subcommand_ != "exists" && subcommand_ != "kill") {
7272
return {Status::RedisReadOnly, "You can't write against a read only slave"};
7373
}
7474

@@ -84,6 +84,12 @@ class CommandScript : public Commander {
8484
return s;
8585
}
8686
*output = redis::RESP_OK;
87+
} else if (args_.size() == 2 && subcommand_ == "kill") {
88+
auto s = srv->ScriptKill();
89+
if (!s) {
90+
return s;
91+
}
92+
*output = redis::RESP_OK;
8793
} else if (args_.size() >= 3 && subcommand_ == "exists") {
8894
*output = redis::MultiLen(args_.size() - 2);
8995
for (size_t j = 2; j < args_.size(); j++) {

src/config/config.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ Config::Config() {
263263
{"skip-block-cache-deallocation-on-close", false, new YesNoField(&skip_block_cache_deallocation_on_close, false)},
264264
{"histogram-bucket-boundaries", true, new StringField(&histogram_bucket_boundaries_str_, "")},
265265
{"lua-strict-key-accessing", false, new YesNoField(&lua_strict_key_accessing, false)},
266+
{"lua-time-limit", false, new IntField(&lua_time_limit, 0, -1, INT_MAX)},
266267

267268
/* rocksdb options */
268269
{"rocksdb.compression", false,

src/config/config.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ struct Config {
205205

206206
bool lua_strict_key_accessing = false;
207207

208+
int lua_time_limit = 0;
209+
208210
std::vector<double> histogram_bucket_boundaries;
209211

210212
struct RocksDB {

src/server/redis_connection.cc

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,28 +94,48 @@ std::string Connection::ToString() {
9494
}
9595

9696
void Connection::Close() {
97-
if (close_cb) close_cb(GetFD());
97+
if (is_running_) {
98+
EnableFlag(kCloseAsync);
99+
if (bev_) {
100+
bufferevent_disable(bev_, EV_READ | EV_WRITE);
101+
bufferevent_setcb(bev_, nullptr, nullptr, nullptr, nullptr);
102+
int fd = bufferevent_getfd(bev_);
103+
if (fd != -1) {
104+
evutil_closesocket(fd);
105+
}
106+
}
107+
return;
108+
}
109+
110+
if (close_cb) {
111+
close_cb(GetFD());
112+
}
98113
owner_->FreeConnection(this);
99114
}
100115

101116
void Connection::Detach() { owner_->DetachConnection(this); }
102117

103118
void Connection::OnRead([[maybe_unused]] struct bufferevent *bev) {
119+
if (is_running_) return;
104120
is_running_ = true;
105-
MakeScopeExit([this] { is_running_ = false; });
106121

107122
SetLastInteraction();
108123
auto s = req_.Tokenize(Input());
109124
if (!s.IsOK()) {
125+
is_running_ = false;
110126
EnableFlag(redis::Connection::kCloseAfterReply);
111127
Reply(redis::Error(s));
112128
INFO("[connection] Failed to tokenize the request. Error: {}", s.Msg());
113129
return;
114130
}
115131

116132
ExecuteCommands(req_.GetCommands());
133+
is_running_ = false;
134+
117135
if (IsFlagEnabled(kCloseAsync)) {
118136
Close();
137+
} else if (evbuffer_get_length(Input()) > 0) {
138+
bufferevent_trigger(bev_, EV_READ, BEV_TRIG_IGNORE_WATERMARKS);
119139
}
120140
}
121141

@@ -433,6 +453,18 @@ void Connection::ExecuteCommands(std::deque<CommandTokens> *to_process_cmds) {
433453
to_process_cmds->pop_front();
434454
if (cmd_tokens.empty()) continue;
435455

456+
bool is_script_kill = (util::EqualICase(cmd_tokens.front(), "script") && cmd_tokens.size() >= 2 &&
457+
util::EqualICase(cmd_tokens[1], "kill"));
458+
bool is_shutdown = util::EqualICase(cmd_tokens.front(), "shutdown");
459+
460+
if (srv_->IsScriptTimedOut()) {
461+
if (!is_script_kill && !is_shutdown) {
462+
Reply(redis::Error({Status::RedisErrorNoPrefix,
463+
"BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE."}));
464+
continue;
465+
}
466+
}
467+
436468
bool is_multi_exec = IsFlagEnabled(Connection::kMultiExec);
437469
if (IsFlagEnabled(redis::Connection::kCloseAfterReply) && !is_multi_exec) break;
438470
auto multi_error_exit = MakeScopeExit([&] {
@@ -457,7 +489,6 @@ void Connection::ExecuteCommands(std::deque<CommandTokens> *to_process_cmds) {
457489
continue;
458490
}
459491
auto current_cmd = std::move(*cmd_s);
460-
461492
const auto &attributes = current_cmd->GetAttributes();
462493
auto cmd_name = attributes->name;
463494

@@ -494,7 +525,9 @@ void Connection::ExecuteCommands(std::deque<CommandTokens> *to_process_cmds) {
494525
// that can guarantee other threads can't come into critical zone, such as DEBUG,
495526
// CLUSTER subcommand, CONFIG SET, MULTI, LUA (in the immediate future).
496527
// Otherwise, we just use 'ConcurrencyGuard' to allow all workers to execute commands at the same time.
497-
if (is_multi_exec && !(cmd_flags & kCmdBypassMulti)) {
528+
if (is_script_kill || is_shutdown) {
529+
// Bypass locks to allow SCRIPT KILL and SHUTDOWN to run even if another thread is stuck in a script
530+
} else if (is_multi_exec && !(cmd_flags & kCmdBypassMulti)) {
498531
// No lock guard, because 'exec' command has acquired 'WorkExclusivityGuard'
499532
} else if (cmd_flags & kCmdExclusive) {
500533
exclusivity = srv_->WorkExclusivityGuard();

src/server/server.cc

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,16 @@ Server::Server(engine::Storage *storage, Config *config)
101101
// init shard pub/sub channels
102102
pubsub_shard_channels_.resize(config->cluster_enabled ? HASH_SLOTS_SIZE : 1);
103103

104+
std::vector<int> listen_fds;
104105
for (int i = 0; i < config->workers; i++) {
105-
auto worker = std::make_unique<Worker>(this, config);
106+
#ifdef __APPLE__
107+
auto worker = std::make_unique<Worker>(this, config, listen_fds);
108+
if (i == 0) {
109+
listen_fds = worker->GetTCPListenFDs();
110+
}
111+
#else
112+
auto worker = std::make_unique<Worker>(this, config, std::vector<int>{});
113+
#endif
106114
// multiple workers can't listen to the same unix socket, so
107115
// listen unix socket only from a single worker - the first one
108116
if (!config->unixsocket.empty() && i == 0) {
@@ -255,6 +263,13 @@ void Server::Stop() {
255263
if (replication_thread_) replication_thread_->Stop();
256264
slaveof_mu_.unlock();
257265

266+
{
267+
std::lock_guard<std::mutex> guard(running_scripts_mu_);
268+
for (auto *rctx : running_scripts_) {
269+
rctx->is_killed = true;
270+
}
271+
}
272+
258273
for (const auto &worker : worker_threads_) {
259274
worker->Stop(0 /* immediately terminate */);
260275
}
@@ -1879,6 +1894,62 @@ StatusOr<std::unique_ptr<redis::Commander>> Server::LookupAndCreateCommand(const
18791894
return std::move(cmd);
18801895
}
18811896

1897+
void Server::RegisterRunningScript(lua::ScriptRunCtx *rctx) {
1898+
std::lock_guard<std::mutex> guard(running_scripts_mu_);
1899+
running_scripts_.push_back(rctx);
1900+
}
1901+
1902+
void Server::UnregisterRunningScript(lua::ScriptRunCtx *rctx) {
1903+
std::lock_guard<std::mutex> guard(running_scripts_mu_);
1904+
auto it = std::find(running_scripts_.begin(), running_scripts_.end(), rctx);
1905+
if (it != running_scripts_.end()) {
1906+
running_scripts_.erase(it);
1907+
}
1908+
1909+
// Re-evaluate if any remaining scripts are timed out, if not, clear the flag
1910+
bool any_timed_out = false;
1911+
int limit = config_->lua_time_limit;
1912+
if (limit > 0 && !running_scripts_.empty()) {
1913+
uint64_t now_ms = util::GetTimeStampMS();
1914+
for (const auto *ctx : running_scripts_) {
1915+
if (now_ms - ctx->start_time_ms >= static_cast<uint64_t>(limit)) {
1916+
any_timed_out = true;
1917+
break;
1918+
}
1919+
}
1920+
}
1921+
is_script_timeout_.store(any_timed_out, std::memory_order_relaxed);
1922+
}
1923+
1924+
bool Server::IsScriptTimedOut() const { return is_script_timeout_.load(std::memory_order_relaxed); }
1925+
1926+
void Server::SetScriptTimedOut(bool timed_out) { is_script_timeout_.store(timed_out, std::memory_order_relaxed); }
1927+
1928+
Status Server::ScriptKill() {
1929+
std::lock_guard<std::mutex> guard(running_scripts_mu_);
1930+
if (running_scripts_.empty()) {
1931+
return {Status::NotOK, "NOTBUSY No scripts in execution right now."};
1932+
}
1933+
1934+
bool has_unkillable = false;
1935+
for (auto *rctx : running_scripts_) {
1936+
if (rctx->is_write_dirty) {
1937+
has_unkillable = true;
1938+
} else {
1939+
rctx->is_killed = true;
1940+
}
1941+
}
1942+
1943+
if (has_unkillable) {
1944+
return {Status::NotOK,
1945+
"UNKILLABLE Sorry the script already executed write commands against the dataset. "
1946+
"You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE "
1947+
"command."};
1948+
}
1949+
1950+
return Status::OK();
1951+
}
1952+
18821953
Status Server::ScriptExists(const std::string &sha) const {
18831954
std::string body;
18841955
return ScriptGet(sha, &body);
@@ -2068,8 +2139,14 @@ void Server::AdjustWorkerThreads() {
20682139
}
20692140

20702141
void Server::increaseWorkerThreads(size_t delta) {
2142+
std::vector<int> listen_fds;
2143+
#ifdef __APPLE__
2144+
if (!worker_threads_.empty()) {
2145+
listen_fds = worker_threads_[0]->GetWorker()->GetTCPListenFDs();
2146+
}
2147+
#endif
20712148
for (size_t i = 0; i < delta; i++) {
2072-
auto worker = std::make_unique<Worker>(this, config_);
2149+
auto worker = std::make_unique<Worker>(this, config_, listen_fds);
20732150
auto worker_thread = std::make_unique<WorkerThread>(std::move(worker));
20742151
worker_thread->Start();
20752152
worker_threads_.emplace_back(std::move(worker_thread));

src/server/server.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@
6262

6363
constexpr const char *REDIS_VERSION = "7.0.0";
6464

65+
namespace lua {
66+
struct ScriptRunCtx;
67+
}
68+
6569
struct DBScanInfo {
6670
// Last scan system clock in seconds
6771
int64_t last_scan_time_secs = 0;
@@ -315,6 +319,12 @@ class Server {
315319
void KillClient(int64_t *killed, const std::string &addr, uint64_t id, uint64_t type, bool skipme,
316320
redis::Connection *conn);
317321

322+
void RegisterRunningScript(lua::ScriptRunCtx *rctx);
323+
void UnregisterRunningScript(lua::ScriptRunCtx *rctx);
324+
bool IsScriptTimedOut() const;
325+
void SetScriptTimedOut(bool timed_out);
326+
Status ScriptKill();
327+
318328
Status ScriptExists(const std::string &sha) const;
319329
Status ScriptGet(const std::string &sha, std::string *body) const;
320330
Status ScriptSet(const std::string &sha, const std::string &body) const;
@@ -478,4 +488,8 @@ class Server {
478488
uint64_t id;
479489
};
480490
std::vector<PausedConnEntry> paused_conns_;
491+
492+
std::vector<lua::ScriptRunCtx *> running_scripts_;
493+
mutable std::mutex running_scripts_mu_;
494+
std::atomic<bool> is_script_timeout_{false};
481495
};

0 commit comments

Comments
 (0)