mirror of
https://github.com/dragonflydb/dragonfly.git
synced 2025-05-10 18:05:44 +02:00
390 lines
12 KiB
C++
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), ¶ms); 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, ¶ms](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
|