| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * |
| */ |
| |
| #include <storage/batch_extractor.h> |
| |
| #include <ctime> |
| |
| #include "command_parser.h" |
| #include "commander.h" |
| #include "commands/scan_base.h" |
| #include "common/io_util.h" |
| #include "common/logging.h" |
| #include "common/rdb_stream.h" |
| #include "common/string_util.h" |
| #include "common/time_util.h" |
| #include "config/config.h" |
| #include "error_constants.h" |
| #include "server/redis_connection.h" |
| #include "server/redis_reply.h" |
| #include "server/server.h" |
| #include "stats/disk_stats.h" |
| #include "storage/rdb/rdb.h" |
| |
| namespace redis { |
| |
| class CommandAuth : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| auto &user_password = args_[1]; |
| std::string ns; |
| AuthResult result = srv->AuthenticateUser(user_password, &ns); |
| switch (result) { |
| case AuthResult::NO_REQUIRE_PASS: |
| return {Status::RedisExecErr, "Client sent AUTH, but no password is set"}; |
| case AuthResult::INVALID_PASSWORD: |
| return {Status::RedisExecErr, "Invalid password"}; |
| case AuthResult::IS_USER: |
| conn->BecomeUser(); |
| break; |
| case AuthResult::IS_ADMIN: |
| conn->BecomeAdmin(); |
| break; |
| } |
| conn->SetNamespace(ns); |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandNamespace : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| Config *config = srv->GetConfig(); |
| std::string sub_command = util::ToLower(args_[1]); |
| if (config->repl_namespace_enabled && config->IsSlave() && sub_command != "get") { |
| return {Status::RedisExecErr, "namespace is read-only for slave"}; |
| } |
| if (config->redis_databases > 0) { |
| return {Status::RedisExecErr, "namespace command is not allowed when redis-databases > 0"}; |
| } |
| if (args_.size() == 3 && sub_command == "get") { |
| if (args_[2] == "*") { |
| std::vector<std::string> namespaces; |
| auto tokens = srv->GetNamespace()->List(); |
| for (auto &token : tokens) { |
| namespaces.emplace_back(token.second); // namespace |
| namespaces.emplace_back(token.first); // token |
| } |
| namespaces.emplace_back(kDefaultNamespace); |
| namespaces.emplace_back(config->requirepass); |
| *output = ArrayOfBulkStrings(namespaces); |
| } else { |
| auto token = srv->GetNamespace()->Get(args_[2]); |
| if (token.Is<Status::NotFound>()) { |
| *output = conn->NilString(); |
| } else { |
| *output = redis::BulkString(token.GetValue()); |
| } |
| } |
| } else if (args_.size() == 4 && sub_command == "set") { |
| Status s = srv->GetNamespace()->Set(args_[2], args_[3]); |
| *output = s.IsOK() ? redis::RESP_OK : redis::Error(s); |
| WARN("Updated namespace: {} with token: {}, addr: {}, result: {}", args_[2], args_[3], conn->GetAddr(), s.Msg()); |
| } else if (args_.size() == 4 && sub_command == "add") { |
| Status s = srv->GetNamespace()->Add(args_[2], args_[3]); |
| *output = s.IsOK() ? redis::RESP_OK : redis::Error(s); |
| WARN("New namespace: {} with token: {}, addr: {}, result: {}", args_[2], args_[3], conn->GetAddr(), s.Msg()); |
| } else if (args_.size() == 3 && sub_command == "del") { |
| Status s = srv->GetNamespace()->Del(args_[2]); |
| *output = s.IsOK() ? redis::RESP_OK : redis::Error(s); |
| WARN("Deleted namespace: {}, addr: {}, result: {}", args_[2], conn->GetAddr(), s.Msg()); |
| } else if (args_.size() == 2 && sub_command == "current") { |
| *output = redis::BulkString(conn->GetNamespace()); |
| } else { |
| return {Status::RedisExecErr, "NAMESPACE subcommand must be one of GET, SET, DEL, ADD and CURRENT"}; |
| } |
| return Status::OK(); |
| } |
| }; |
| |
| static uint64_t GenerateNamespaceFlag(uint64_t flags, const std::vector<std::string> &args) { |
| if (args.size() >= 2 && util::EqualICase(args[1], "current")) { |
| return flags & ~kCmdAdmin; |
| } |
| |
| return flags; |
| } |
| |
| class CommandKeys : public Commander { |
| public: |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| const std::string &glob_pattern = args_[1]; |
| std::vector<std::string> keys; |
| redis::Database redis(srv->storage, conn->GetNamespace()); |
| |
| if (const Status s = util::ValidateGlob(glob_pattern); !s.IsOK()) { |
| return {Status::RedisParseErr, "Invalid glob pattern: " + s.Msg()}; |
| } |
| const auto [prefix, suffix_glob] = util::SplitGlob(glob_pattern); |
| const rocksdb::Status s = redis.Keys(ctx, prefix, suffix_glob, &keys); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| *output = conn->MultiBulkString(keys); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandFlushDB : public Commander { |
| public: |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| if (srv->GetConfig()->cluster_enabled) { |
| if (srv->slot_migrator->IsMigrationInProgress()) { |
| srv->slot_migrator->SetStopMigrationFlag(true); |
| INFO("Stop migration task for flushdb"); |
| } |
| } |
| redis::Database redis(srv->storage, conn->GetNamespace()); |
| |
| auto s = redis.FlushDB(ctx); |
| WARN("DB keys in namespace: {} was flushed, addr: {}", conn->GetNamespace(), conn->GetAddr()); |
| if (s.ok()) { |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } |
| |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| }; |
| |
| class CommandFlushAll : public Commander { |
| public: |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| if (srv->GetConfig()->cluster_enabled) { |
| if (srv->slot_migrator->IsMigrationInProgress()) { |
| srv->slot_migrator->SetStopMigrationFlag(true); |
| INFO("Stop migration task for flushall"); |
| } |
| } |
| |
| redis::Database redis(srv->storage, conn->GetNamespace()); |
| |
| auto s = redis.FlushAll(ctx); |
| if (s.ok()) { |
| WARN("All DB keys was flushed, addr: {}", conn->GetAddr()); |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } |
| |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| }; |
| |
| class CommandPing : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, [[maybe_unused]] Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| if (args_.size() == 1) { |
| *output = redis::SimpleString("PONG"); |
| } else if (args_.size() == 2) { |
| *output = redis::BulkString(args_[1]); |
| } else { |
| return {Status::NotOK, errWrongNumOfArguments}; |
| } |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandSelect : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| Config *config = srv->GetConfig(); |
| // If redis-databases is 0, just return OK (default behavior) |
| if (config->redis_databases == 0) { |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } |
| |
| // Parse database index |
| auto parse_result = ParseInt<int>(args_[1], 10); |
| if (!parse_result) { |
| return {Status::RedisParseErr, "Invalid DB number"}; |
| } |
| int db_index = *parse_result; |
| // Validate database index range |
| if (db_index < 0 || db_index >= config->redis_databases) { |
| return {Status::RedisParseErr, "DB number is out of range"}; |
| } |
| |
| // DB 0 uses default namespace |
| std::string ns = kDefaultNamespace; |
| if (db_index > 0) { |
| ns = std::string(kDatabaseNamespacePrefix) + std::to_string(db_index); |
| } |
| conn->SetNamespace(ns); |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandConfig : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| Config *config = srv->GetConfig(); |
| std::string sub_command = util::ToLower(args_[1]); |
| if ((sub_command == "rewrite" && args_.size() != 2) || (sub_command == "get" && args_.size() != 3) || |
| (sub_command == "set" && args_.size() != 4)) { |
| return {Status::RedisExecErr, errWrongNumOfArguments}; |
| } |
| |
| if (args_.size() == 2 && sub_command == "rewrite") { |
| Status s = config->Rewrite(srv->GetNamespace()->List()); |
| if (!s.IsOK()) return s; |
| |
| *output = redis::RESP_OK; |
| INFO("# CONFIG REWRITE executed with success"); |
| } else if (args_.size() == 3 && sub_command == "get") { |
| std::vector<std::string> values; |
| config->Get(args_[2], &values); |
| *output = conn->MapOfBulkStrings(values); |
| } else if (args_.size() == 4 && sub_command == "set") { |
| Status s = config->Set(srv, args_[2], args_[3]); |
| if (!s.IsOK()) { |
| return {Status::RedisExecErr, "CONFIG SET '" + args_[2] + "' error: " + s.Msg()}; |
| } else { |
| *output = redis::RESP_OK; |
| } |
| } else { |
| return {Status::RedisExecErr, "CONFIG subcommand must be one of GET, SET, REWRITE"}; |
| } |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandInfo : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| std::vector<std::string> sections; |
| for (size_t i = 1; i < args_.size(); ++i) { |
| sections.push_back(args_[i]); |
| } |
| auto info = srv->GetInfo(conn->GetNamespace(), sections); |
| *output = conn->VerbatimString("txt", info); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandDisk : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| std::string opname = util::ToLower(args[1]); |
| if (opname != "usage") return {Status::RedisInvalidCmd, "Unknown operation"}; |
| return Commander::Parse(args); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| RedisType type = kRedisNone; |
| redis::Disk disk_db(srv->storage, conn->GetNamespace()); |
| |
| auto s = disk_db.Type(ctx, args_[2], &type); |
| if (!s.ok()) return {Status::RedisExecErr, s.ToString()}; |
| |
| uint64_t result = 0; |
| s = disk_db.GetKeySize(ctx, args_[2], type, &result); |
| if (!s.ok()) { |
| // Redis returns the Nil string when the key does not exist |
| if (s.IsNotFound()) { |
| *output = conn->NilString(); |
| return Status::OK(); |
| } |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| *output = redis::Integer(result); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandMemory : public CommandDisk {}; |
| |
| class CommandRole : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| *output = srv->GetRoleInfo(); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandDBSize : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| std::string ns = conn->GetNamespace(); |
| if (args_.size() == 1) { |
| KeyNumStats stats; |
| srv->GetLatestKeyNumStats(ns, &stats); |
| *output = redis::Integer(stats.n_key); |
| } else if (args_.size() == 2 && util::EqualICase(args_[1], "scan")) { |
| Status s = srv->AsyncScanDBSize(ns); |
| if (s.IsOK()) { |
| *output = redis::RESP_OK; |
| } else { |
| return s; |
| } |
| } else { |
| return {Status::RedisExecErr, "DBSIZE subcommand only supports scan"}; |
| } |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandKProfile : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| CommandParser parser(args, 1); |
| if (!parser.EatEqICase("memory")) { |
| return {Status::NotOK, "only supports MEMORY subcommand"}; |
| } |
| if (parser.EatEqICase("enable")) { |
| op_ = "enable"; |
| } else if (parser.EatEqICase("disable")) { |
| op_ = "disable"; |
| } else if (parser.EatEqICase("status")) { |
| op_ = "status"; |
| } else if (parser.EatEqICase("dump")) { |
| op_ = "dump"; |
| if (!parser.Good()) return {Status::NotOK, errWrongNumOfArguments}; |
| dump_dir_ = GET_OR_RET(parser.TakeStr()); |
| } else { |
| return {Status::NotOK, "MEMORY subcommand must be one of ENABLE, DISABLE, STATUS, DUMP"}; |
| } |
| if (parser.Good()) { |
| return {Status::NotOK, errWrongNumOfArguments}; |
| } |
| return Status::OK(); |
| } |
| |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| Status s; |
| if (op_ == "enable" || op_ == "disable") { |
| s = srv->memory_profiler.SetProfiling(op_ == "enable"); |
| } else if (op_ == "status") { |
| auto enabled = GET_OR_RET(srv->memory_profiler.GetProfilingStatus()); |
| *output = SimpleString(enabled ? "enabled" : "disabled"); |
| return Status::OK(); |
| } else if (op_ == "dump") { |
| bool is_dir = false; |
| if (auto s = rocksdb::Env::Default()->IsDirectory(dump_dir_, &is_dir); !s.ok()) { |
| return {Status::NotOK, fmt::format("\"{}\" is not a directory", dump_dir_)}; |
| } |
| s = srv->memory_profiler.Dump(dump_dir_); |
| } else { |
| return {Status::NotOK, "MEMORY subcommand must be one of ENABLE, DISABLE, STATUS, DUMP"}; |
| } |
| |
| if (!s.IsOK()) return s; |
| *output = RESP_OK; |
| return Status::OK(); |
| } |
| |
| private: |
| std::string op_; |
| std::string dump_dir_; |
| }; |
| |
| class CommandPerfLog : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| subcommand_ = util::ToLower(args[1]); |
| if (subcommand_ != "reset" && subcommand_ != "get" && subcommand_ != "len") { |
| return {Status::NotOK, "PERFLOG subcommand must be one of RESET, LEN, GET"}; |
| } |
| |
| if (subcommand_ == "get" && args.size() >= 3) { |
| if (args[2] == "*") { |
| cnt_ = 0; |
| } else { |
| cnt_ = GET_OR_RET(ParseInt<int64_t>(args[2], 10)); |
| } |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| auto perf_log = srv->GetPerfLog(); |
| if (subcommand_ == "len") { |
| *output = redis::Integer(static_cast<int64_t>(perf_log->Size())); |
| } else if (subcommand_ == "reset") { |
| perf_log->Reset(); |
| *output = redis::RESP_OK; |
| } else if (subcommand_ == "get") { |
| *output = perf_log->GetLatestEntries(cnt_); |
| } |
| return Status::OK(); |
| } |
| |
| private: |
| std::string subcommand_; |
| int64_t cnt_ = 10; |
| }; |
| |
| class CommandSlowlog : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| subcommand_ = util::ToLower(args[1]); |
| if (subcommand_ != "reset" && subcommand_ != "get" && subcommand_ != "len") { |
| return {Status::NotOK, "SLOWLOG subcommand must be one of RESET, LEN, GET"}; |
| } |
| |
| if (subcommand_ == "get" && args.size() >= 3) { |
| if (args[2] == "*") { |
| cnt_ = 0; |
| } else { |
| cnt_ = GET_OR_RET(ParseInt<int64_t>(args[2], 10)); |
| } |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| auto slowlog = srv->GetSlowLog(); |
| if (subcommand_ == "reset") { |
| slowlog->Reset(); |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } else if (subcommand_ == "len") { |
| *output = redis::Integer(static_cast<int64_t>(slowlog->Size())); |
| return Status::OK(); |
| } else if (subcommand_ == "get") { |
| *output = slowlog->GetLatestEntries(cnt_); |
| return Status::OK(); |
| } |
| return {Status::NotOK, "SLOWLOG subcommand must be one of RESET, LEN, GET"}; |
| } |
| |
| private: |
| std::string subcommand_; |
| int64_t cnt_ = 10; |
| }; |
| |
| class CommandClient : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| subcommand_ = util::ToLower(args[1]); |
| // subcommand: getname id kill list info setname reply |
| if ((subcommand_ == "id" || subcommand_ == "getname" || subcommand_ == "list" || subcommand_ == "info") && |
| args.size() == 2) { |
| return Status::OK(); |
| } |
| |
| if ((subcommand_ == "setname") && args.size() == 3) { |
| // Check if the charset is ok. We need to do this otherwise |
| // CLIENT LIST or CLIENT INFO format will break. You should always be able to |
| // split by space to get the different fields. |
| for (auto ch : args[2]) { |
| if (ch < '!' || ch > '~') { |
| return {Status::RedisInvalidCmd, "Client names cannot contain spaces, newlines or special characters"}; |
| } |
| } |
| |
| conn_name_ = args[2]; |
| return Status::OK(); |
| } |
| |
| if (subcommand_ == "setinfo") { |
| if (args.size() != 4) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| auto attr = util::ToLower(args[2]); |
| |
| for (auto ch : args[3]) { |
| if (ch < '!' || ch > '~') { |
| return {Status::RedisInvalidCmd, |
| "lib-name and lib-ver cannot contain spaces, newlines or special characters"}; |
| } |
| } |
| |
| if (attr == "lib-name") { |
| setinfo_lib_name_ = args[3]; |
| } else if (attr == "lib-ver") { |
| setinfo_lib_ver_ = args[3]; |
| } else { |
| return {Status::RedisInvalidCmd, "Unrecognized option '" + args[2] + "'"}; |
| } |
| |
| return Status::OK(); |
| } |
| |
| if (subcommand_ == "reply") { |
| if (args.size() != 3) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| auto mode_str = util::ToLower(args[2]); |
| if (mode_str == "on") { |
| reply_mode_ = redis::Connection::ReplyMode::ON; |
| } else if (mode_str == "off") { |
| reply_mode_ = redis::Connection::ReplyMode::OFF; |
| } else if (mode_str == "skip") { |
| reply_mode_ = redis::Connection::ReplyMode::SKIP; |
| } else { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| return Status::OK(); |
| } |
| |
| if (subcommand_ == "unpause") { |
| if (args.size() != 2) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| return Status::OK(); |
| } |
| |
| if (subcommand_ == "pause") { |
| if (args.size() < 3 || args.size() > 4) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| auto parse_result = ParseInt<uint64_t>(args[2], 10); |
| if (!parse_result) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| pause_timeout_ms_ = *parse_result; |
| pause_mode_ = PauseMode::kAll; |
| if (args.size() == 4) { |
| std::string mode = util::ToLower(args[3]); |
| if (mode == "write") { |
| pause_mode_ = PauseMode::kWrite; |
| } else if (mode != "all") { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| } |
| return Status::OK(); |
| } |
| |
| if ((subcommand_ == "kill")) { |
| if (args.size() == 2) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| if (args.size() == 3) { |
| addr_ = args[2]; |
| new_format_ = false; |
| return Status::OK(); |
| } |
| |
| size_t i = 2; |
| new_format_ = true; |
| |
| while (i < args.size()) { |
| bool more_args = i < args.size(); |
| if (util::EqualICase(args[i], "addr") && more_args) { |
| addr_ = args[i + 1]; |
| } else if (util::EqualICase(args[i], "id") && more_args) { |
| auto parse_result = ParseInt<uint64_t>(args[i + 1], 10); |
| if (!parse_result) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| |
| id_ = *parse_result; |
| } else if (util::EqualICase(args[i], "skipme") && more_args) { |
| if (util::EqualICase(args[i + 1], "yes")) { |
| skipme_ = true; |
| } else if (util::EqualICase(args[i + 1], "no")) { |
| skipme_ = false; |
| } else { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| } else if (util::EqualICase(args[i], "type") && more_args) { |
| if (util::EqualICase(args[i + 1], "normal")) { |
| kill_type_ |= kTypeNormal; |
| } else if (util::EqualICase(args[i + 1], "pubsub")) { |
| kill_type_ |= kTypePubsub; |
| } else if (util::EqualICase(args[i + 1], "master")) { |
| kill_type_ |= kTypeMaster; |
| } else if (util::EqualICase(args[i + 1], "replica") || util::EqualICase(args[i + 1], "slave")) { |
| kill_type_ |= kTypeSlave; |
| } else { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| } else { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| i += 2; |
| } |
| return Status::OK(); |
| } |
| return {Status::RedisInvalidCmd, |
| "Syntax error, try CLIENT LIST|INFO|KILL ip:port|GETNAME|SETNAME|SETINFO|REPLY|" |
| "PAUSE|UNPAUSE"}; |
| } |
| |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| if (subcommand_ == "list") { |
| *output = conn->VerbatimString("txt", srv->GetClientsStr(conn)); |
| return Status::OK(); |
| } else if (subcommand_ == "info") { |
| *output = conn->VerbatimString("txt", conn->ToString()); |
| return Status::OK(); |
| } else if (subcommand_ == "setname") { |
| conn->SetName(conn_name_); |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } else if (subcommand_ == "getname") { |
| std::string name = conn->GetName(); |
| *output = name == "" ? conn->NilString() : redis::BulkString(name); |
| return Status::OK(); |
| } else if (subcommand_ == "id") { |
| *output = redis::Integer(conn->GetID()); |
| return Status::OK(); |
| } else if (subcommand_ == "kill") { |
| int64_t killed = 0; |
| srv->KillClient(&killed, addr_, id_, kill_type_, skipme_, conn); |
| if (new_format_) { |
| *output = redis::Integer(killed); |
| } else { |
| if (killed == 0) |
| return {Status::RedisExecErr, "No such client"}; |
| else |
| *output = redis::RESP_OK; |
| } |
| return Status::OK(); |
| } else if (subcommand_ == "reply") { |
| conn->SetReplyMode(reply_mode_); |
| if (reply_mode_ != redis::Connection::ReplyMode::SKIP) { |
| *output = redis::RESP_OK; |
| } |
| return Status::OK(); |
| } else if (subcommand_ == "pause") { |
| if (!conn->IsAdmin()) { |
| return {Status::RedisExecErr, errAdminPermissionRequired}; |
| } |
| uint64_t now_ms = util::GetTimeStampMS(); |
| srv->PauseConns(now_ms + pause_timeout_ms_, pause_mode_); |
| WARN("CLIENT PAUSE executed, timeout={}ms, mode={}, addr: {}", pause_timeout_ms_, |
| pause_mode_ == PauseMode::kWrite ? "write" : "all", conn->GetAddr()); |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } else if (subcommand_ == "unpause") { |
| if (!conn->IsAdmin()) { |
| return {Status::RedisExecErr, errAdminPermissionRequired}; |
| } |
| srv->UnpauseConns(); |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } else if (subcommand_ == "setinfo") { |
| if (setinfo_lib_name_) { |
| conn->SetLibName(*setinfo_lib_name_); |
| } |
| if (setinfo_lib_ver_) { |
| conn->SetLibVer(*setinfo_lib_ver_); |
| } |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } |
| |
| return {Status::RedisInvalidCmd, |
| "Syntax error, try CLIENT LIST|INFO|KILL ip:port|GETNAME|SETNAME|SETINFO|REPLY|" |
| "PAUSE|UNPAUSE"}; |
| } |
| |
| private: |
| std::string addr_; |
| std::string conn_name_; |
| std::string subcommand_; |
| std::optional<std::string> setinfo_lib_name_; |
| std::optional<std::string> setinfo_lib_ver_; |
| redis::Connection::ReplyMode reply_mode_ = redis::Connection::ReplyMode::ON; |
| bool skipme_ = false; |
| int64_t kill_type_ = 0; |
| uint64_t id_ = 0; |
| bool new_format_ = true; |
| uint64_t pause_timeout_ms_ = 0; |
| PauseMode pause_mode_ = PauseMode::kAll; |
| }; |
| |
| class CommandMonitor : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, [[maybe_unused]] Server *srv, Connection *conn, |
| std::string *output) override { |
| conn->Owner()->BecomeMonitorConn(conn); |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandShutdown : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, [[maybe_unused]] Connection *conn, |
| [[maybe_unused]] std::string *output) override { |
| if (!srv->IsStopped()) { |
| INFO("SHUTDOWN command received, stopping the server"); |
| srv->Stop(); |
| } |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandQuit : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, [[maybe_unused]] Server *srv, Connection *conn, |
| std::string *output) override { |
| conn->EnableFlag(redis::Connection::kCloseAfterReply); |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandDebug : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| subcommand_ = util::ToLower(args[1]); |
| if ((subcommand_ == "sleep") && args.size() == 3) { |
| auto second = ParseFloat(args[2]); |
| if (!second) { |
| return {Status::RedisParseErr, "invalid debug sleep time"}; |
| } |
| |
| microsecond_ = static_cast<uint64_t>(*second * 1000 * 1000); |
| return Status::OK(); |
| } else if (subcommand_ == "protocol" && args.size() == 3) { |
| protocol_type_ = util::ToLower(args[2]); |
| return Status::OK(); |
| } else if (subcommand_ == "dbsize-limit" && args.size() == 3) { |
| auto val = ParseInt<int32_t>(args[2], {0, 1}, 10); |
| if (!val) { |
| return {Status::RedisParseErr, "invalid debug dbsize-limit value"}; |
| } |
| |
| dbsize_limit_ = static_cast<bool>(val); |
| return Status::OK(); |
| } |
| return {Status::RedisInvalidCmd, "Syntax error, DEBUG SLEEP <seconds>|PROTOCOL <type>|DBSIZE-LIMIT <0|1>"}; |
| } |
| |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| if (subcommand_ == "sleep") { |
| usleep(microsecond_); |
| *output = redis::RESP_OK; |
| } else if (subcommand_ == "protocol") { // protocol type |
| if (protocol_type_ == "string") { |
| *output = redis::BulkString("Hello World"); |
| } else if (protocol_type_ == "integer") { |
| *output = redis::Integer(12345); |
| } else if (protocol_type_ == "double") { |
| *output = conn->Double(3.141); |
| } else if (protocol_type_ == "array") { |
| *output = redis::MultiLen(3); |
| for (int i = 0; i < 3; i++) { |
| *output += redis::Integer(i); |
| } |
| } else if (protocol_type_ == "set") { |
| *output = conn->HeaderOfSet(3); |
| for (int i = 0; i < 3; i++) { |
| *output += redis::Integer(i); |
| } |
| } else if (protocol_type_ == "map") { |
| *output = conn->HeaderOfMap(3); |
| for (int i = 0; i < 3; i++) { |
| *output += redis::Integer(i); |
| *output += conn->Bool(i == 1); |
| } |
| } else if (protocol_type_ == "bignum") { |
| *output = conn->BigNumber("1234567999999999999999999999999999999"); |
| } else if (protocol_type_ == "true") { |
| *output = conn->Bool(true); |
| } else if (protocol_type_ == "false") { |
| *output = conn->Bool(false); |
| } else if (protocol_type_ == "null") { |
| *output = conn->NilString(); |
| } else if (protocol_type_ == "attrib") { |
| *output = conn->HeaderOfAttribute(1); |
| *output += redis::BulkString("key-popularity"); |
| *output += redis::Array({ |
| redis::BulkString("key:123"), |
| redis::Integer(90), |
| }); |
| } else if (protocol_type_ == "verbatim") { // verbatim string |
| *output = conn->VerbatimString("txt", "verbatim string"); |
| } else { |
| return {Status::RedisErrorNoPrefix, |
| "Wrong protocol type name. Please use one of the following: " |
| "string|integer|double|array|set|bignum|true|false|null|attrib|verbatim"}; |
| } |
| } else if (subcommand_ == "dbsize-limit") { |
| srv->storage->SetDBSizeLimit(dbsize_limit_); |
| *output = redis::RESP_OK; |
| } else { |
| return {Status::RedisInvalidCmd, "Unknown subcommand, should be SLEEP, PROTOCOL or DBSIZE-LIMIT"}; |
| } |
| return Status::OK(); |
| } |
| |
| static uint64_t FlagGen(uint64_t flags, const std::vector<std::string> &args) { |
| if (args.size() >= 2 && util::EqualICase(args[1], "protocol")) { |
| return flags & ~kCmdExclusive; |
| } |
| |
| return flags; |
| } |
| |
| private: |
| std::string subcommand_; |
| std::string protocol_type_; |
| uint64_t microsecond_ = 0; |
| bool dbsize_limit_ = false; |
| }; |
| |
| class CommandCommand : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, [[maybe_unused]] Server *srv, Connection *conn, |
| std::string *output) override { |
| if (args_.size() == 1) { |
| CommandTable::GetAllCommandsInfo(output); |
| } else { |
| std::string sub_command = util::ToLower(args_[1]); |
| if ((sub_command == "count" && args_.size() != 2) || (sub_command == "getkeys" && args_.size() < 3) || |
| (sub_command == "info" && args_.size() < 3)) { |
| return {Status::RedisExecErr, errWrongNumOfArguments}; |
| } |
| |
| if (sub_command == "count") { |
| *output = redis::Integer(CommandTable::Size()); |
| } else if (sub_command == "info") { |
| CommandTable::GetCommandsInfo(output, std::vector<std::string>(args_.begin() + 2, args_.end())); |
| } else if (sub_command == "getkeys") { |
| auto cmd_iter = CommandTable::GetOriginal()->find(util::ToLower(args_[2])); |
| if (cmd_iter == CommandTable::GetOriginal()->end()) { |
| return {Status::RedisUnknownCmd, "Invalid command specified"}; |
| } |
| |
| auto key_indexes = GET_OR_RET(CommandTable::GetKeysFromCommand( |
| cmd_iter->second, std::vector<std::string>(args_.begin() + 2, args_.end()))); |
| |
| if (key_indexes.size() == 0) { |
| return {Status::RedisExecErr, "Invalid arguments specified for command"}; |
| } |
| |
| std::vector<std::string> keys; |
| keys.reserve(key_indexes.size()); |
| for (const auto &key_index : key_indexes) { |
| keys.emplace_back(args_[key_index + 2]); |
| } |
| *output = conn->MultiBulkString(keys); |
| } else { |
| return {Status::RedisExecErr, "Command subcommand must be one of COUNT, GETKEYS, INFO"}; |
| } |
| } |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandEcho : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, [[maybe_unused]] Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| *output = redis::BulkString(args_[1]); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandTime : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, [[maybe_unused]] Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| uint64_t now = util::GetTimeStampUS(); |
| uint64_t s = now / 1000 / 1000; // unix time in seconds. |
| uint64_t us = now - (s * 1000 * 1000); // microseconds. |
| |
| *output = redis::MultiLen(2); |
| *output += redis::BulkString(std::to_string(s)); |
| *output += redis::BulkString(std::to_string(us)); |
| |
| return Status::OK(); |
| } |
| }; |
| |
| /* |
| * HELLO [<protocol-version> [AUTH [<password>|<username> <password>]] [SETNAME <name>] ] |
| * Note that the <username> should always be `default` if provided otherwise AUTH fails. |
| * And it is only meant to be aligning syntax with Redis HELLO. |
| */ |
| class CommandHello final : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| size_t next_arg = 1; |
| int protocol = 2; // default protocol version is 2 |
| if (args_.size() >= 2) { |
| auto parse_result = ParseInt<int>(args_[next_arg], 10); |
| ++next_arg; |
| if (!parse_result) { |
| return {Status::NotOK, "Protocol version is not an integer or out of range"}; |
| } |
| |
| protocol = *parse_result; |
| |
| // In redis, it will check protocol < 2 or protocol > 3, |
| // kvrocks only supports REPL2 by now, but for supporting some |
| // `hello 3`, it will not report error when using 3. |
| if (protocol < 2 || protocol > 3) { |
| return {Status::RedisNoProto, "unsupported protocol version"}; |
| } |
| } |
| |
| // Handling AUTH and SETNAME |
| for (; next_arg < args_.size(); ++next_arg) { |
| size_t more_args = args_.size() - next_arg - 1; |
| const std::string &opt = args_[next_arg]; |
| if (util::ToLower(opt) == "auth" && more_args != 0) { |
| if (more_args == 2 || more_args == 4) { |
| if (args_[next_arg + 1] != "default") { |
| return {Status::NotOK, "Invalid password"}; |
| } |
| next_arg++; |
| } |
| const auto &user_password = args_[next_arg + 1]; |
| std::string ns; |
| AuthResult auth_result = srv->AuthenticateUser(user_password, &ns); |
| switch (auth_result) { |
| case AuthResult::NO_REQUIRE_PASS: |
| return {Status::NotOK, "Client sent AUTH, but no password is set"}; |
| case AuthResult::INVALID_PASSWORD: |
| return {Status::NotOK, "Invalid password"}; |
| case AuthResult::IS_USER: |
| conn->BecomeUser(); |
| break; |
| case AuthResult::IS_ADMIN: |
| conn->BecomeAdmin(); |
| break; |
| } |
| conn->SetNamespace(ns); |
| next_arg += 1; |
| } else if (util::ToLower(opt) == "setname" && more_args != 0) { |
| const std::string &name = args_[next_arg + 1]; |
| conn->SetName(name); |
| next_arg += 1; |
| } else { |
| return {Status::RedisExecErr, "Syntax error in HELLO option " + opt}; |
| } |
| } |
| |
| std::vector<std::string> output_list; |
| output_list.push_back(redis::BulkString("server")); |
| output_list.push_back(redis::BulkString("redis")); |
| output_list.push_back(redis::BulkString("version")); |
| // What the client want is the Redis compatible version instead of the Kvrocks version. |
| output_list.push_back(redis::BulkString(REDIS_VERSION)); |
| output_list.push_back(redis::BulkString("proto")); |
| if (srv->GetConfig()->resp3_enabled) { |
| output_list.push_back(redis::Integer(protocol)); |
| conn->SetProtocolVersion(protocol == 3 ? RESP::v3 : RESP::v2); |
| } else { |
| output_list.push_back(redis::Integer(2)); |
| } |
| |
| output_list.push_back(redis::BulkString("mode")); |
| // Note: sentinel is not supported in kvrocks. |
| if (srv->GetConfig()->cluster_enabled) { |
| output_list.push_back(redis::BulkString("cluster")); |
| } else { |
| output_list.push_back(redis::BulkString("standalone")); |
| } |
| output_list.push_back(redis::BulkString("role")); |
| output_list.push_back(redis::BulkString(srv->IsSlave() ? "slave" : "master")); |
| // For Kvrocks, the modules is not supported. |
| output_list.push_back(redis::BulkString("modules")); |
| output_list.push_back(conn->NilArray()); |
| *output = conn->HeaderOfMap(output_list.size() / 2); |
| for (const auto &item : output_list) { |
| *output += item; |
| } |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandScan : public CommandScanBase { |
| public: |
| CommandScan() : CommandScanBase() {} |
| |
| static std::string GenerateOutput(Server *srv, [[maybe_unused]] const Connection *conn, |
| const std::vector<std::string> &keys, const std::string &end_cursor) { |
| std::vector<std::string> list; |
| if (!end_cursor.empty()) { |
| list.emplace_back( |
| redis::BulkString(srv->GenerateCursorFromKeyName(end_cursor, CursorType::kTypeBase, kCursorPrefix))); |
| } else { |
| list.emplace_back(redis::BulkString("0")); |
| } |
| |
| list.emplace_back(ArrayOfBulkStrings(keys)); |
| |
| return redis::Array(list); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Database redis_db(srv->storage, conn->GetNamespace()); |
| auto key_name = srv->GetKeyNameFromCursor(cursor_, CursorType::kTypeBase); |
| |
| std::vector<std::string> keys; |
| std::string end_key; |
| |
| const auto s = redis_db.Scan(ctx, key_name, limit_, prefix_, suffix_glob_, &keys, &end_key, type_, scan_slot_); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| *output = GenerateOutput(srv, conn, keys, end_key); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandRandomKey : public Commander { |
| public: |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| std::string key; |
| auto cursor = srv->GetLastRandomKeyCursor(); |
| redis::Database redis(srv->storage, conn->GetNamespace()); |
| |
| auto s = redis.RandomKey(ctx, cursor, &key); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| srv->SetLastRandomKeyCursor(key); |
| *output = redis::BulkString(key); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandCompact : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| std::string begin_key, end_key; |
| auto ns = conn->GetNamespace(); |
| if (ns != kDefaultNamespace) { |
| begin_key = ComposeNamespaceKey(ns, "", false); |
| end_key = util::StringNext(begin_key); |
| } |
| |
| Status s = srv->AsyncCompactDB(begin_key, end_key); |
| if (!s.IsOK()) return s; |
| |
| *output = redis::RESP_OK; |
| INFO("Compact was triggered by manual with executed success"); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandBGSave : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| Status s = srv->AsyncBgSaveDB(); |
| if (!s.IsOK()) return s; |
| |
| *output = redis::RESP_OK; |
| INFO("BGSave was triggered by manual with executed success"); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandFlushBackup : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| Status s = srv->AsyncPurgeOldBackups(0, 0); |
| if (!s.IsOK()) return s; |
| |
| *output = redis::RESP_OK; |
| INFO("flushbackup was triggered by manual with executed success"); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandSlaveOf : public Commander { |
| public: |
| static Status IsTryingToReplicateItself(Server *srv, const std::string &host, uint32_t port) { |
| auto ip_addresses = util::LookupHostByName(host); |
| if (!ip_addresses) { |
| return {Status::NotOK, "Can not resolve hostname: " + host}; |
| } |
| for (auto &ip : *ip_addresses) { |
| if (util::MatchListeningIP(srv->GetConfig()->binds, ip) && port == srv->GetConfig()->port) { |
| return {Status::NotOK, "can't replicate itself"}; |
| } |
| for (std::pair<std::string, uint32_t> &host_port_pair : srv->GetSlaveHostAndPort()) { |
| if (host_port_pair.first == ip && host_port_pair.second == port) { |
| return {Status::NotOK, "can't replicate your own replicas"}; |
| } |
| } |
| } |
| return Status::OK(); |
| } |
| |
| Status Parse(const std::vector<std::string> &args) override { |
| host_ = args[1]; |
| const auto &port = args[2]; |
| if (util::ToLower(host_) == "no" && util::ToLower(port) == "one") { |
| host_.clear(); |
| return Status::OK(); |
| } |
| |
| auto parse_result = ParseInt<uint32_t>(port, 10); |
| if (!parse_result) { |
| return {Status::RedisParseErr, "port should be number"}; |
| } |
| |
| port_ = *parse_result; |
| return Commander::Parse(args); |
| } |
| |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| if (srv->GetConfig()->cluster_enabled) { |
| return {Status::RedisExecErr, "can't change to slave in cluster mode"}; |
| } |
| |
| if (srv->GetConfig()->rocks_db.write_options.disable_wal) { |
| return {Status::RedisExecErr, "slaveof doesn't work with disable_wal option"}; |
| } |
| |
| if (host_.empty()) { |
| auto s = srv->RemoveMaster(); |
| if (!s.IsOK()) { |
| return s.Prefixed("failed to remove master"); |
| } |
| |
| *output = redis::RESP_OK; |
| WARN("MASTER MODE enabled (user request from '{}')", conn->GetAddr()); |
| return Status::OK(); |
| } |
| |
| auto s = IsTryingToReplicateItself(srv, host_, port_); |
| if (!s.IsOK()) return s; |
| s = srv->AddMaster(host_, port_, false); |
| if (s.IsOK()) { |
| *output = redis::RESP_OK; |
| WARN("SLAVE OF {}:{} enabled (user request from '{}')", host_, port_, conn->GetAddr()); |
| } else { |
| ERROR("SLAVE OF {}:{} (user request from '{}') encounter error: {}", host_, port_, conn->GetAddr(), s.Msg()); |
| } |
| |
| return s; |
| } |
| |
| private: |
| std::string host_; |
| uint32_t port_ = 0; |
| }; |
| |
| class CommandStats : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| std::string stats_json = srv->GetRocksDBStatsJson(); |
| *output = redis::BulkString(stats_json); |
| return Status::OK(); |
| } |
| }; |
| |
| static uint64_t GenerateConfigFlag(uint64_t flags, const std::vector<std::string> &args) { |
| if (args.size() >= 2 && util::EqualICase(args[1], "set")) { |
| return flags | kCmdExclusive; |
| } |
| |
| return flags; |
| } |
| |
| class CommandLastSave : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| CommandParser parser(args, 1); |
| if (args.size() > 2) { |
| return {Status::RedisParseErr, "unknown extra arguments"}; |
| } |
| while (parser.Good()) { |
| if (parser.EatEqICase("iso8601")) { |
| format_spec_ = true; |
| } else { |
| return {Status::RedisParseErr, "unknown arguments"}; |
| } |
| } |
| |
| return Status::OK(); |
| } |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| int64_t unix_sec = srv->GetLastBgsaveTime(); |
| if (format_spec_) { |
| auto raw_time = static_cast<time_t>(unix_sec); |
| tm local_time{}; |
| if (localtime_r(&raw_time, &local_time)) { |
| char buf[64]; |
| strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S%z", &local_time); |
| *output = redis::BulkString(buf); |
| } else { |
| return {Status::NotOK, "unable to convert timestamp to local time"}; |
| } |
| } else { |
| *output = redis::Integer(unix_sec); |
| } |
| return Status::OK(); |
| } |
| |
| private: |
| bool format_spec_ = false; |
| }; |
| |
| class CommandRestore : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| CommandParser parser(args, 4); |
| ttl_ms_ = GET_OR_RET(ParseInt<int64_t>(args[2], {0, INT64_MAX}, 10)); |
| while (parser.Good()) { |
| if (parser.EatEqICase("replace")) { |
| replace_ = true; |
| } else if (parser.EatEqICase("absttl")) { |
| absttl_ = true; |
| } else if (parser.EatEqICase("idletime")) { |
| // idle time is not supported in Kvrocks, so just skip it |
| auto idle_time = GET_OR_RET(parser.TakeInt()); |
| if (idle_time < 0) { |
| return {Status::RedisParseErr, "IDLETIME can't be negative"}; |
| } |
| } else if (parser.EatEqICase("freq")) { |
| // freq is not supported in Kvrocks, so just skip it |
| auto freq = GET_OR_RET(parser.TakeInt()); |
| if (freq < 0 || freq > 255) { |
| return {Status::RedisParseErr, "FREQ must be >= 0 and <= 255"}; |
| } |
| } else { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| } |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| rocksdb::Status db_status; |
| redis::Database redis(srv->storage, conn->GetNamespace()); |
| |
| if (!replace_) { |
| uint32_t count = 0; |
| db_status = redis.Exists(ctx, {args_[1]}, &count); |
| if (!db_status.ok()) { |
| return {Status::RedisExecErr, db_status.ToString()}; |
| } |
| if (count > 0) { |
| return {Status::RedisExecErr, "target key name already exists."}; |
| } |
| } else { |
| db_status = redis.Del(ctx, args_[1]); |
| if (!db_status.ok() && !db_status.IsNotFound()) { |
| return {Status::RedisExecErr, db_status.ToString()}; |
| } |
| } |
| if (ttl_ms_ && absttl_) { |
| auto now = util::GetTimeStampMS(); |
| if (ttl_ms_ <= now) { |
| // return ok if the ttl is already expired |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } |
| ttl_ms_ -= now; |
| } |
| |
| auto stream_ptr = std::make_unique<RdbStringStream>(args_[3]); |
| RDB rdb(srv->storage, conn->GetNamespace(), std::move(stream_ptr)); |
| auto s = rdb.Restore(ctx, args_[1], args_[3], ttl_ms_); |
| if (!s.IsOK()) return s; |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } |
| |
| private: |
| bool replace_ = false; |
| bool absttl_ = false; |
| uint64_t ttl_ms_ = 0; |
| }; |
| |
| // command format: rdb load <path> [NX] [DB index] |
| class CommandRdb : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| CommandParser parser(args, 1); |
| |
| type_ = GET_OR_RET(parser.TakeStr()); |
| if (!util::EqualICase(type_, "load")) { |
| return {Status::RedisParseErr, "unknown subcommand"}; |
| } |
| |
| path_ = GET_OR_RET(parser.TakeStr()); |
| while (parser.Good()) { |
| if (parser.EatEqICase("NX")) { |
| overwrite_exist_key_ = false; |
| } else if (parser.EatEqICase("DB")) { |
| db_index_ = GET_OR_RET(parser.TakeInt<uint32_t>()); |
| } else { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Database redis(srv->storage, conn->GetNamespace()); |
| |
| auto stream_ptr = std::make_unique<RdbFileStream>(path_); |
| GET_OR_RET(stream_ptr->Open()); |
| |
| RDB rdb(srv->storage, conn->GetNamespace(), std::move(stream_ptr)); |
| GET_OR_RET(rdb.LoadRdb(ctx, db_index_, overwrite_exist_key_)); |
| |
| *output = redis::RESP_OK; |
| return Status::OK(); |
| } |
| |
| private: |
| std::string type_; |
| std::string path_; |
| bool overwrite_exist_key_ = true; // default overwrite exist key |
| uint32_t db_index_ = 0; |
| }; |
| |
| class CommandReset : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| // 1. Discards the current MULTI transaction block, if one exists. |
| if (conn->IsFlagEnabled(Connection::kMultiExec)) { |
| conn->ResetMultiExec(); |
| } |
| // 2. Unwatches all keys WATCHed by the connection. |
| srv->ResetWatchedKeys(conn); |
| // 3. Disables CLIENT TRACKING, if in use. (not yet supported) |
| // 4. Sets the connection to READWRITE mode. |
| // 5. Cancels the connection's ASKING mode, if previously set. (not yet supported) |
| // 6. Sets CLIENT REPLY to ON. (not yet supported) |
| // 9. Exits MONITOR mode, when applicable. |
| if (conn->IsFlagEnabled(Connection::kMonitor)) { |
| conn->Owner()->QuitMonitorConn(conn); |
| } |
| // 10. Aborts Pub/Sub's subscription state (SUBSCRIBE and PSUBSCRIBE), when appropriate. |
| if (conn->SubscriptionsCount() != 0) { |
| conn->UnsubscribeAll(); |
| } |
| if (conn->PSubscriptionsCount() != 0) { |
| conn->PUnsubscribeAll(); |
| } |
| // 11. Deauthenticates the connection, requiring a call AUTH to reauthenticate when authentication is enabled. |
| conn->SetNamespace(kDefaultNamespace); |
| conn->BecomeAdmin(); |
| // 12. Turns off NO-EVICT / NO-TOUCH mode. (not yet supported) |
| *output = redis::SimpleString("RESET"); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandApplyBatch : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| raw_batch_ = args[1]; |
| if (args.size() > 2) { |
| if (args.size() > 3) { |
| return {Status::RedisParseErr, errWrongNumOfArguments}; |
| } |
| if (!util::EqualICase(args[2], "lowpri")) { |
| return {Status::RedisParseErr, "only support LOWPRI option"}; |
| } |
| low_pri_ = true; |
| } |
| return Commander::Parse(args); |
| } |
| |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *svr, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| size_t size = raw_batch_.size(); |
| auto options = svr->storage->DefaultWriteOptions(); |
| options.low_pri = low_pri_; |
| auto s = svr->storage->ApplyWriteBatch(options, std::move(raw_batch_)); |
| if (!s.IsOK()) return s; |
| |
| *output = redis::Integer(size); |
| return Status::OK(); |
| } |
| |
| private: |
| std::string raw_batch_; |
| bool low_pri_ = false; |
| }; |
| |
| class CommandDump : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| if (args.size() != 2) { |
| return {Status::RedisExecErr, errWrongNumOfArguments}; |
| } |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| rocksdb::Status db_status; |
| std::string &key = args_[1]; |
| redis::Database redis(srv->storage, conn->GetNamespace()); |
| uint32_t count = 0; |
| |
| db_status = redis.Exists(ctx, {key}, &count); |
| if (!db_status.ok()) { |
| if (db_status.IsNotFound()) { |
| *output = conn->NilString(); |
| return Status::OK(); |
| } |
| return {Status::RedisExecErr, db_status.ToString()}; |
| } |
| if (count == 0) { |
| *output = conn->NilString(); |
| return Status::OK(); |
| } |
| |
| RedisType type = kRedisNone; |
| db_status = redis.Type(ctx, key, &type); |
| if (!db_status.ok()) return {Status::RedisExecErr, db_status.ToString()}; |
| |
| std::string result; |
| auto stream_ptr = std::make_unique<RdbStringStream>(result); |
| RDB rdb(srv->storage, conn->GetNamespace(), std::move(stream_ptr)); |
| auto s = rdb.Dump(key, type); |
| if (!s.IsOK()) return s; |
| CHECK(dynamic_cast<RdbStringStream *>(rdb.GetStream().get()) != nullptr); |
| *output = redis::BulkString(static_cast<RdbStringStream *>(rdb.GetStream().get())->GetInput()); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandPollUpdates : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| CommandParser parser(args, 1); |
| sequence_ = GET_OR_RET(parser.TakeInt<uint64_t>()); |
| |
| while (parser.Good()) { |
| if (parser.EatEqICase("MAX")) { |
| max_ = GET_OR_RET(parser.TakeInt<int64_t>(NumericRange<int64_t>{1, 1000})); |
| } else if (parser.EatEqICase("STRICT")) { |
| is_strict_ = true; |
| } else if (parser.EatEqICase("FORMAT")) { |
| auto format = GET_OR_RET(parser.TakeStr()); |
| if (util::EqualICase(format, "RAW")) { |
| format_ = Format::Raw; |
| } else if (util::EqualICase(format, "RESP")) { |
| format_ = Format::RESP; |
| } else { |
| return {Status::RedisParseErr, "invalid FORMAT option, should be RAW or RESP"}; |
| } |
| } else { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| } |
| return Status::OK(); |
| } |
| |
| static StatusOr<std::string> ToRespFormat(const engine::Storage *storage, |
| const std::vector<rocksdb::BatchResult> &batches) { |
| std::map<std::string, std::vector<std::string>> ns_commands; |
| for (const auto &batch : batches) { |
| rocksdb::WriteBatch write_batch(batch.writeBatchPtr->Data()); |
| WriteBatchExtractor extractor(storage->IsSlotIdEncoded(), -1, true); |
| auto db_status = write_batch.Iterate(&extractor); |
| if (!db_status.ok()) { |
| return {Status::RedisExecErr, db_status.ToString()}; |
| } |
| auto parsed_commands = extractor.GetRESPCommands(); |
| for (const auto &iter : *parsed_commands) { |
| // Add the command vector to the namespace |
| ns_commands[iter.first].insert(ns_commands[iter.first].end(), iter.second.begin(), iter.second.end()); |
| } |
| } |
| |
| std::string updates = MultiLen(ns_commands.size() * 2); |
| for (const auto &iter : ns_commands) { |
| if (iter.first == kDefaultNamespace) { |
| updates += BulkString("default"); |
| } else { |
| updates += BulkString(iter.first); |
| } |
| updates += Array(iter.second); |
| } |
| return updates; |
| } |
| |
| static StatusOr<std::string> ToRawFormat(const std::vector<rocksdb::BatchResult> &batches) { |
| std::string updates = redis::MultiLen(batches.size()); |
| for (const auto &batch : batches) { |
| updates += BulkString(util::StringToHex(batch.writeBatchPtr->Data())); |
| } |
| return updates; |
| } |
| |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| uint64_t next_sequence = sequence_; |
| // sequence + 1 is for excluding the current sequence to avoid getting duplicate updates |
| auto batches = GET_OR_RET(srv->PollUpdates(sequence_ + 1, max_, is_strict_)); |
| if (batches.size() > 0) { |
| auto &last_batch = batches.back(); |
| next_sequence = last_batch.sequence + last_batch.writeBatchPtr->Count() - 1; |
| } |
| std::string updates; |
| if (format_ == Format::RESP) { |
| updates = GET_OR_RET(ToRespFormat(srv->storage, batches)); |
| } else { |
| updates = GET_OR_RET(ToRawFormat(batches)); |
| } |
| *output = conn->Map({ |
| {redis::BulkString("latest_sequence"), redis::Integer(srv->storage->LatestSeqNumber())}, |
| {redis::BulkString("updates"), std::move(updates)}, |
| {redis::BulkString("next_sequence"), redis::Integer(next_sequence)}, |
| }); |
| return Status::OK(); |
| } |
| |
| private: |
| enum class Format { |
| Raw, |
| RESP, |
| }; |
| |
| uint64_t sequence_ = -1; |
| bool is_strict_ = false; |
| int64_t max_ = 16; |
| Format format_ = Format::Raw; |
| }; |
| |
| class CommandSST : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| CommandParser parser(args, 1); |
| std::string cmd = GET_OR_RET(parser.TakeStr()); |
| if (!util::EqualICase(cmd, "load")) { |
| return {Status::RedisParseErr, "unknown subcommand:" + args[1]}; |
| } |
| folder_ = GET_OR_RET(parser.TakeStr()); |
| // Parse optional movefiles flag |
| while (parser.Good()) { |
| if (parser.EatEqICase("movefiles")) { |
| std::string move_files = GET_OR_RET(parser.TakeStr()); |
| if (util::EqualICase(move_files, "yes")) { |
| ingest_options_.move_files = true; |
| } else if (util::EqualICase(move_files, "no")) { |
| ingest_options_.move_files = false; |
| } else { |
| return {Status::RedisParseErr, "movefiles value must be 'yes' or 'no'"}; |
| } |
| } else { |
| return {Status::RedisParseErr, "unknown option: " + parser.TakeStr().GetValue()}; |
| } |
| } |
| return Commander::Parse(args); |
| } |
| |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| if (srv->GetConfig()->cluster_enabled) { |
| return {Status::NotOK, "The SST command is not supported in cluster mode."}; |
| } |
| if (srv->IsSlave()) { |
| return {Status::NotOK, "Replica nodes do not support the SST command"}; |
| } |
| if (srv->GetReplicaCount() != 0) { |
| return {Status::NotOK, "The SST command is not supported when there are replicas."}; |
| } |
| auto s = srv->storage->IngestSST(folder_, ingest_options_); |
| if (!s.IsOK()) return {Status::RedisExecErr, s.Msg()}; |
| *output = conn->Map({{redis::BulkString("files_loaded"), redis::Integer(s.GetValue())}}); |
| return Status::OK(); |
| } |
| |
| private: |
| std::string folder_; |
| rocksdb::IngestExternalFileOptions ingest_options_; |
| }; |
| |
| // command format: flushmemtable [async] |
| class CommandFlushMemTable : public Commander { |
| public: |
| Status Parse([[maybe_unused]] const std::vector<std::string> &args) override { |
| if (args.size() > 2) { |
| return {Status::RedisParseErr, errWrongNumOfArguments}; |
| } |
| |
| flush_options_.wait = true; |
| if (args.size() > 1) { |
| if (util::EqualICase(args[1], "async")) { |
| flush_options_.wait = false; |
| } else { |
| return {Status::RedisParseErr, "parameter must be 'ASYNC'"}; |
| } |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| const auto s = srv->storage->FlushMemTable(nullptr, flush_options_); |
| if (!s.ok()) return {Status::RedisExecErr, s.ToString()}; |
| |
| *output = redis::RESP_OK; |
| INFO("FLUSHMEMTABLE is triggered and executed successfully"); |
| return Status::OK(); |
| } |
| |
| private: |
| rocksdb::FlushOptions flush_options_; |
| }; |
| |
| class CommandFlushBlockCache : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, [[maybe_unused]] Connection *conn, |
| std::string *output) override { |
| srv->storage->FlushBlockCache(); |
| |
| *output = redis::RESP_OK; |
| INFO("FLUSHBLOCKCACHE is triggered and executed successfully"); |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandLatency : public Commander { |
| public: |
| Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| if (args_.size() < 2) { |
| return {Status::RedisParseErr, errWrongNumOfArguments}; |
| } |
| |
| std::string subcommand = util::ToLower(args_[1]); |
| if (subcommand == "histogram") { |
| return getHistogram(srv, conn, output); |
| } else if (subcommand == "reset") { |
| *output = redis::Integer(0); |
| return Status::OK(); |
| } else if (subcommand == "help") { |
| std::vector<std::string> help = { |
| "HELP", |
| " Print this help message.", |
| "HISTOGRAM [command ...]", |
| " Return a cumulative distribution of latencies in the format of a histogram for the specified " |
| "command(s).", |
| " If no commands are specified, all commands with latency statistics are returned.", |
| "RESET [event ...]", |
| " Return the number of reset event classes (Kvrocks has no spike-sampling infrastructure, always returns " |
| "0)."}; |
| *output = ArrayOfBulkStrings(help); |
| return Status::OK(); |
| } |
| |
| return {Status::RedisParseErr, "Unknown LATENCY subcommand or wrong number of arguments"}; |
| } |
| |
| private: |
| Status getHistogram(Server *srv, Connection *conn, std::string *output) { |
| if (srv->stats.bucket_boundaries.empty()) { |
| *output = conn->HeaderOfMap(0); |
| return Status::OK(); |
| } |
| |
| std::vector<const std::pair<const std::string, CommandHistogram> *> target_histograms; |
| if (args_.size() > 2) { |
| for (size_t i = 2; i < args_.size(); i++) { |
| auto it = srv->stats.commands_histogram.find(util::ToLower(args_[i])); |
| if (it != srv->stats.commands_histogram.end() && it->second.calls > 0) { |
| target_histograms.push_back(&(*it)); |
| } |
| } |
| } else { |
| for (const auto &iter : srv->stats.commands_histogram) { |
| if (iter.second.calls > 0) { |
| target_histograms.push_back(&iter); |
| } |
| } |
| } |
| |
| *output = conn->HeaderOfMap(target_histograms.size()); |
| for (const auto *pair_ptr : target_histograms) { |
| const auto &cmd_name = pair_ptr->first; |
| const auto &hist = pair_ptr->second; |
| |
| std::vector<std::pair<int64_t, uint64_t>> cumulative_buckets; |
| uint64_t cumulative = 0; |
| for (size_t i = 0; i < hist.buckets.size(); i++) { |
| cumulative += hist.buckets[i]->load(std::memory_order_relaxed); |
| if (cumulative == 0) continue; |
| |
| int64_t boundary = 0; |
| if (i < srv->stats.bucket_boundaries.size()) { |
| boundary = static_cast<int64_t>(srv->stats.bucket_boundaries[i]); |
| } else { |
| boundary = -1; |
| } |
| cumulative_buckets.emplace_back(boundary, cumulative); |
| } |
| |
| *output += redis::BulkString(cmd_name); |
| *output += conn->HeaderOfMap(2); |
| |
| *output += redis::BulkString("calls"); |
| *output += redis::Integer(hist.calls.load(std::memory_order_relaxed)); |
| |
| *output += redis::BulkString("histogram_usec"); |
| *output += conn->HeaderOfMap(cumulative_buckets.size()); |
| for (const auto &[boundary, count] : cumulative_buckets) { |
| if (boundary < 0) { |
| *output += redis::BulkString("inf"); |
| } else { |
| *output += redis::Integer(boundary); |
| } |
| *output += redis::Integer(count); |
| } |
| } |
| |
| return Status::OK(); |
| } |
| }; |
| |
| REDIS_REGISTER_COMMANDS( |
| Server, MakeCmdAttr<CommandAuth>("auth", 2, "read-only ok-loading auth", NO_KEY), |
| MakeCmdAttr<CommandPing>("ping", -1, "read-only", NO_KEY), |
| MakeCmdAttr<CommandSelect>("select", 2, "read-only", NO_KEY), |
| MakeCmdAttr<CommandInfo>("info", -1, "read-only ok-loading", NO_KEY), |
| MakeCmdAttr<CommandRole>("role", 1, "read-only ok-loading", NO_KEY), |
| MakeCmdAttr<CommandConfig>("config", -2, "read-only admin skip-monitor", NO_KEY, GenerateConfigFlag), |
| MakeCmdAttr<CommandNamespace>("namespace", -2, "read-only admin skip-monitor", NO_KEY, GenerateNamespaceFlag), |
| MakeCmdAttr<CommandKeys>("keys", 2, "read-only slow", NO_KEY), |
| MakeCmdAttr<CommandFlushDB>("flushdb", 1, "write no-dbsize-check exclusive", NO_KEY), |
| MakeCmdAttr<CommandFlushAll>("flushall", 1, "write no-dbsize-check exclusive admin", NO_KEY), |
| MakeCmdAttr<CommandDBSize>("dbsize", -1, "read-only", NO_KEY), |
| MakeCmdAttr<CommandSlowlog>("slowlog", -2, "read-only", NO_KEY), |
| MakeCmdAttr<CommandKProfile>("kprofile", -3, "read-only admin", NO_KEY), |
| MakeCmdAttr<CommandPerfLog>("perflog", -2, "read-only", NO_KEY), |
| MakeCmdAttr<CommandClient>("client", -2, "read-only", NO_KEY), |
| MakeCmdAttr<CommandMonitor>("monitor", 1, "read-only no-multi no-script", NO_KEY), |
| MakeCmdAttr<CommandShutdown>("shutdown", 1, "read-only exclusive no-multi no-script admin", NO_KEY), |
| MakeCmdAttr<CommandQuit>("quit", 1, "read-only", NO_KEY), MakeCmdAttr<CommandScan>("scan", -2, "read-only", NO_KEY), |
| MakeCmdAttr<CommandRandomKey>("randomkey", 1, "read-only", NO_KEY), |
| MakeCmdAttr<CommandDebug>("debug", -2, "read-only exclusive", NO_KEY, CommandDebug::FlagGen), |
| MakeCmdAttr<CommandCommand>("command", -1, "read-only", NO_KEY), |
| MakeCmdAttr<CommandEcho>("echo", 2, "read-only", NO_KEY), |
| MakeCmdAttr<CommandTime>("time", 1, "read-only ok-loading", NO_KEY), |
| MakeCmdAttr<CommandDisk>("disk", 3, "read-only", 2, 2, 1), |
| MakeCmdAttr<CommandMemory>("memory", 3, "read-only", 2, 2, 1), |
| MakeCmdAttr<CommandHello>("hello", -1, "read-only ok-loading auth", NO_KEY), |
| MakeCmdAttr<CommandRestore>("restore", -4, "write", 1, 1, 1), |
| |
| MakeCmdAttr<CommandCompact>("compact", 1, "read-only no-script", NO_KEY), |
| MakeCmdAttr<CommandBGSave>("bgsave", 1, "read-only no-script admin", NO_KEY), |
| MakeCmdAttr<CommandLastSave>("lastsave", -1, "read-only admin", NO_KEY), |
| MakeCmdAttr<CommandFlushBackup>("flushbackup", 1, "read-only no-script admin", NO_KEY), |
| MakeCmdAttr<CommandSlaveOf>("slaveof", 3, "read-only exclusive no-script admin", NO_KEY), |
| MakeCmdAttr<CommandSlaveOf>("replicaof", 3, "read-only exclusive no-script admin", NO_KEY), |
| MakeCmdAttr<CommandStats>("stats", 1, "read-only", NO_KEY), |
| MakeCmdAttr<CommandRdb>("rdb", -3, "write exclusive admin", NO_KEY), |
| MakeCmdAttr<CommandReset>("reset", 1, "ok-loading bypass-multi no-script admin", NO_KEY), |
| MakeCmdAttr<CommandApplyBatch>("applybatch", -2, "write no-multi admin", NO_KEY), |
| MakeCmdAttr<CommandDump>("dump", 2, "read-only", 1, 1, 1), |
| MakeCmdAttr<CommandPollUpdates>("pollupdates", -2, "read-only admin", NO_KEY), |
| MakeCmdAttr<CommandSST>("sst", -3, "write exclusive admin", 1, 1, 1), |
| MakeCmdAttr<CommandFlushMemTable>("flushmemtable", -1, "exclusive write", NO_KEY), |
| MakeCmdAttr<CommandFlushBlockCache>("flushblockcache", 1, "exclusive write", NO_KEY), |
| MakeCmdAttr<CommandLatency>("latency", -2, "read-only admin", NO_KEY), ) |
| } // namespace redis |