dragonfly/src/server/script_mgr.cc
adiholden 2d49a28c15
fix(server): handle running script load inside multi (#4074)
Signed-off-by: adi_holden <adi@dragonflydb.io>
2024-11-10 09:34:40 +02:00

390 lines
12 KiB
C++

// Copyright 2022, DragonflyDB authors. All rights reserved.
// See LICENSE for licensing terms.
//
#include "server/script_mgr.h"
#include <absl/cleanup/cleanup.h>
#include <absl/strings/ascii.h>
#include <absl/strings/match.h>
#include <absl/strings/numbers.h>
#include <absl/strings/str_cat.h>
#include <absl/strings/str_split.h>
#include <regex>
#include <string>
#include "base/flags.h"
#include "base/logging.h"
#include "core/interpreter.h"
#include "facade/error.h"
#include "server/engine_shard_set.h"
#include "server/server_state.h"
#include "server/transaction.h"
ABSL_FLAG(std::string, default_lua_flags, "",
"Configure default flags for running Lua scripts: \n - Use 'allow-undeclared-keys' to "
"allow accessing undeclared keys, \n - Use 'disable-atomicity' to allow "
"running scripts non-atomically. \nSpecify multiple values "
"separated by space, for example 'allow-undeclared-keys disable-atomicity' runs scripts "
"non-atomically and allows accessing undeclared keys");
ABSL_FLAG(
bool, lua_auto_async, false,
"If enabled, call/pcall with discarded values are automatically replaced with acall/apcall.");
ABSL_FLAG(bool, lua_allow_undeclared_auto_correct, false,
"If enabled, when a script that is not allowed to run with undeclared keys is trying to "
"access undeclared keys, automaticaly set the script flag to be able to run with "
"undeclared key.");
ABSL_FLAG(
std::vector<std::string>, lua_undeclared_keys_shas,
std::vector<std::string>({
"351130589c64523cb98978dc32c64173a31244f3", // Sidekiq, see #2442
"6ae15ef4678593dc61f991c9953722d67d822776", // Sidekiq, see #2442
}),
"Comma-separated list of Lua script SHAs which are allowed to access undeclared keys. SHAs are "
"only looked at when loading the script, and new values do not affect already-loaded script.");
namespace dfly {
using namespace std;
using namespace facade;
using namespace util;
ScriptMgr::ScriptMgr() {
// Build default script flags
string flags = absl::GetFlag(FLAGS_default_lua_flags);
static_assert(ScriptParams{}.atomic && !ScriptParams{}.undeclared_keys);
auto err = ScriptParams::ApplyFlags(flags, &default_params_);
CHECK(!err) << err.Format();
}
ScriptMgr::ScriptKey::ScriptKey(string_view sha) : array{} {
DCHECK_EQ(sha.size(), size());
memcpy(data(), sha.data(), size());
}
void ScriptMgr::Run(CmdArgList args, Transaction* tx, SinkReplyBuilder* builder,
ConnectionContext* cntx) {
string subcmd = absl::AsciiStrToUpper(ArgS(args, 0));
if (subcmd == "HELP") {
string_view kHelp[] = {
"SCRIPT <subcommand> [<arg> [value] [opt] ...]",
"Subcommands are:",
"EXISTS <sha1> [<sha1> ...]",
" Return information about the existence of the scripts in the script cache.",
"FLUSH",
" Flush the Lua scripts cache. Very dangerous on replicas.",
"LOAD <script>",
" Load a script into the scripts cache without executing it.",
"FLAGS <sha> [flags ...]",
" Set specific flags for script. Can be called before the sript is loaded.",
" The following flags are possible: ",
" - Use 'allow-undeclared-keys' to allow accessing undeclared keys",
" - Use 'disable-atomicity' to allow running scripts non-atomically",
"LIST",
" Lists loaded scripts.",
"LATENCY",
" Prints latency histograms in usec for every called function.",
"GC",
" Invokes garbage collection on all unused interpreter instances.",
"HELP",
" Prints this help."};
auto rb = static_cast<RedisReplyBuilder*>(builder);
return rb->SendSimpleStrArr(kHelp);
}
if (subcmd == "EXISTS" && args.size() > 1)
return ExistsCmd(args, tx, builder);
if (subcmd == "FLUSH")
return FlushCmd(args, tx, builder);
if (subcmd == "LIST")
return ListCmd(tx, builder);
if (subcmd == "LATENCY")
return LatencyCmd(tx, builder);
if (subcmd == "LOAD" && args.size() == 2)
return LoadCmd(args, tx, builder, cntx);
if (subcmd == "FLAGS" && args.size() > 2)
return ConfigCmd(args, tx, builder);
if (subcmd == "GC")
return GCCmd(tx, builder);
string err = absl::StrCat("Unknown subcommand or wrong number of arguments for '", subcmd,
"'. Try SCRIPT HELP.");
builder->SendError(err, kSyntaxErrType);
}
void ScriptMgr::ExistsCmd(CmdArgList args, Transaction* tx, SinkReplyBuilder* builder) const {
vector<uint8_t> res(args.size() - 1, 0);
for (size_t i = 1; i < args.size(); ++i) {
if (string_view sha = ArgS(args, i); Find(sha)) {
res[i - 1] = 1;
}
}
auto rb = static_cast<RedisReplyBuilder*>(builder);
rb->StartArray(res.size());
for (uint8_t v : res) {
rb->SendLong(v);
}
}
void ScriptMgr::FlushCmd(CmdArgList args, Transaction* tx, SinkReplyBuilder* builder) {
FlushAllScript();
return builder->SendOk();
}
void ScriptMgr::LoadCmd(CmdArgList args, Transaction* tx, SinkReplyBuilder* builder,
ConnectionContext* cntx) {
string_view body = ArgS(args, 1);
auto rb = static_cast<RedisReplyBuilder*>(builder);
if (body.empty()) {
char sha[41];
Interpreter::FuncSha1(body, sha);
return rb->SendBulkString(sha);
}
BorrowedInterpreter interpreter{tx, &cntx->conn_state};
auto res = Insert(body, interpreter);
if (!res)
return builder->SendError(res.error().Format());
// Schedule empty callback inorder to journal command via transaction framework.
tx->ScheduleSingleHop([](auto* t, auto* shard) { return OpStatus::OK; });
return rb->SendBulkString(res.value());
}
void ScriptMgr::ConfigCmd(CmdArgList args, Transaction* tx, SinkReplyBuilder* builder) {
lock_guard lk{mu_};
ScriptKey key{ArgS(args, 1)};
auto& data = db_[key];
for (auto flag : args.subspan(2)) {
if (auto err = ScriptParams::ApplyFlags(facade::ToSV(flag), &data); err)
return builder->SendError("Invalid config format: " + err.Format());
}
UpdateScriptCaches(key, data);
// Schedule empty callback inorder to journal command via transaction framework.
tx->ScheduleSingleHop([](auto* t, auto* shard) { return OpStatus::OK; });
return builder->SendOk();
}
void ScriptMgr::ListCmd(Transaction* tx, SinkReplyBuilder* builder) const {
vector<pair<string, ScriptData>> scripts = GetAll();
auto rb = static_cast<RedisReplyBuilder*>(builder);
rb->StartArray(scripts.size());
for (const auto& [sha, data] : scripts) {
rb->StartArray(2);
rb->SendBulkString(sha);
rb->SendBulkString(data.body);
}
}
void ScriptMgr::LatencyCmd(Transaction* tx, SinkReplyBuilder* builder) const {
absl::flat_hash_map<std::string, base::Histogram> result;
fb2::Mutex mu;
shard_set->pool()->AwaitFiberOnAll([&](auto* pb) {
auto* ss = ServerState::tlocal();
mu.lock();
for (const auto& k_v : ss->call_latency_histos()) {
result[k_v.first].Merge(k_v.second);
}
mu.unlock();
});
auto rb = static_cast<RedisReplyBuilder*>(builder);
rb->StartArray(result.size());
for (const auto& k_v : result) {
rb->StartArray(2);
rb->SendBulkString(k_v.first);
rb->SendVerbatimString(k_v.second.ToString());
}
}
void ScriptMgr::GCCmd(Transaction* tx, SinkReplyBuilder* builder) const {
auto cb = [](Interpreter* ir) {
ir->RunGC();
ThisFiber::Yield();
};
shard_set->pool()->AwaitFiberOnAll(
[cb](auto* pb) { ServerState::tlocal()->AlterInterpreters(cb); });
return builder->SendOk();
}
// Check if script starts with lua flags instructions (--df flags=...).
io::Result<optional<ScriptMgr::ScriptParams>, GenericError> DeduceParams(string_view body) {
static const regex kRegex{R"(^\s*?--!df flags=([^\s\n\r]*)[\s\n\r])"};
cmatch matches;
if (!regex_search(body.data(), matches, kRegex))
return nullopt;
ScriptMgr::ScriptParams params;
if (auto err = ScriptMgr::ScriptParams::ApplyFlags(matches.str(1), &params); err)
return nonstd::make_unexpected(err);
return params;
}
unique_ptr<char[]> CharBufFromSV(string_view sv) {
auto ptr = make_unique<char[]>(sv.size() + 1);
memcpy(ptr.get(), sv.data(), sv.size());
ptr[sv.size()] = '\0';
return ptr;
}
io::Result<string, GenericError> ScriptMgr::Insert(string_view body, Interpreter* interpreter) {
char sha_buf[64];
Interpreter::FuncSha1(body, sha_buf);
string_view sha{sha_buf, std::strlen(sha_buf)};
if (interpreter->Exists(sha)) {
return string{sha};
}
auto params_opt = DeduceParams(body);
if (!params_opt)
return params_opt.get_unexpected();
auto params = params_opt->value_or(default_params_);
auto undeclared_shas = absl::GetFlag(FLAGS_lua_undeclared_keys_shas);
if (find(undeclared_shas.begin(), undeclared_shas.end(), sha) != undeclared_shas.end()) {
params.undeclared_keys = true;
}
// If the script is atomic, check for possible squashing optimizations.
// For non atomic modes, squashing increases the time locks are held, which
// can decrease throughput with frequently accessed keys.
optional<string> async_body;
if (params.atomic && absl::GetFlag(FLAGS_lua_auto_async)) {
if (async_body = Interpreter::DetectPossibleAsyncCalls(body); async_body)
body = *async_body;
}
string result;
Interpreter::AddResult add_result = interpreter->AddFunction(sha, body, &result);
if (add_result == Interpreter::COMPILE_ERR)
return nonstd::make_unexpected(GenericError{std::move(result)});
lock_guard lk{mu_};
auto [it, _] = db_.emplace(sha, InternalScriptData{params, nullptr});
if (!it->second.body) {
it->second.body = CharBufFromSV(body);
}
UpdateScriptCaches(sha, it->second);
return string{sha};
}
optional<ScriptMgr::ScriptData> ScriptMgr::Find(std::string_view sha) const {
if (sha.size() != ScriptKey{}.size())
return std::nullopt;
lock_guard lk{mu_};
if (auto it = db_.find(sha); it != db_.end() && it->second.body)
return ScriptData{it->second, it->second.body.get()};
return std::nullopt;
}
void ScriptMgr::OnScriptError(std::string_view sha, std::string_view error) {
++tl_facade_stats->reply_stats.script_error_count;
// Log script errors at most 5 times a second.
LOG_EVERY_T(WARNING, 0.2) << "Error running script (call to " << sha << "): " << error;
// If script has undeclared_keys and was not flaged to run in this mode we will change the
// script flag - this will make script next run to not fail but run as global.
if (absl::GetFlag(FLAGS_lua_allow_undeclared_auto_correct)) {
size_t pos = error.rfind(kUndeclaredKeyErr);
lock_guard lk{mu_};
auto it = db_.find(sha);
if (it == db_.end()) {
return;
}
if (pos != string::npos) {
it->second.undeclared_keys = true;
LOG(WARNING) << "Setting undeclared_keys flag for script with sha : (" << sha << ")";
UpdateScriptCaches(sha, it->second);
}
}
}
void ScriptMgr::FlushAllScript() {
lock_guard lk{mu_};
db_.clear();
shard_set->pool()->AwaitFiberOnAll([](auto* pb) {
ServerState* ss = ServerState::tlocal();
ss->ResetInterpreter();
});
}
vector<pair<string, ScriptMgr::ScriptData>> ScriptMgr::GetAll() const {
vector<pair<string, ScriptData>> res;
lock_guard lk{mu_};
res.reserve(db_.size());
for (const auto& [sha, data] : db_) {
string body = data.body ? string{data.body.get()} : string{};
res.emplace_back(string{sha.data(), sha.size()}, ScriptData{data, std::move(body)});
}
return res;
}
void ScriptMgr::UpdateScriptCaches(ScriptKey sha, ScriptParams params) const {
shard_set->pool()->AwaitBrief([&sha, &params](auto index, auto* pb) {
ServerState::tlocal()->SetScriptParams(sha, params);
});
}
bool ScriptMgr::AreGlobalByDefault() const {
return default_params_.undeclared_keys && default_params_.atomic;
}
GenericError ScriptMgr::ScriptParams::ApplyFlags(string_view config, ScriptParams* params) {
auto parts = absl::StrSplit(config, absl::ByAnyChar(",; "), absl::SkipEmpty());
for (auto flag : parts) {
if (flag == "disable-atomicity") {
params->atomic = false;
continue;
}
if (flag == "allow-undeclared-keys") {
params->undeclared_keys = true;
continue;
}
if (flag == "no-writes") { // Used by Redis.
// TODO: lock read-only.
continue;
}
return GenericError{"Invalid flag: "s + string{flag}};
}
return {};
}
} // namespace dfly