// Copyright 2022, DragonflyDB authors. All rights reserved. // See LICENSE for licensing terms. // #include "server/debugcmd.h" extern "C" { #include "redis/redis_aux.h" } #include #include #include #include #include #include #include "base/flags.h" #include "base/logging.h" #include "core/compact_object.h" #include "core/string_map.h" #include "server/blocking_controller.h" #include "server/container_utils.h" #include "server/engine_shard_set.h" #include "server/error.h" #include "server/main_service.h" #include "server/multi_command_squasher.h" #include "server/rdb_load.h" #include "server/server_state.h" #include "server/string_family.h" #include "server/transaction.h" using namespace std; ABSL_DECLARE_FLAG(string, dir); ABSL_DECLARE_FLAG(string, dbfilename); ABSL_DECLARE_FLAG(bool, df_snapshot_format); namespace dfly { using namespace util; using boost::intrusive_ptr; using namespace facade; namespace fs = std::filesystem; using absl::GetFlag; using absl::StrAppend; using absl::StrCat; namespace { struct PopulateBatch { DbIndex dbid; uint64_t index[32]; uint64_t sz = 0; PopulateBatch(DbIndex id) : dbid(id) { } }; struct ObjInfo { unsigned encoding; unsigned bucket_id = 0; unsigned slot_id = 0; enum LockStatus { NONE, S, X } lock_status = NONE; int64_t ttl = INT64_MAX; optional external_len; bool has_sec_precision = false; bool found = false; }; struct ValueCompressInfo { size_t raw_size = 0; size_t compressed_size = 0; }; std::string GenerateValue(size_t val_size, bool random_value, absl::InsecureBitGen* gen) { if (random_value) { return GetRandomHex(*gen, val_size); } else { return string(val_size, 'x'); } } tuple> GeneratePopulateCommand( string_view type, std::string key, size_t val_size, bool random_value, uint32_t elements, const CommandRegistry& registry, absl::InsecureBitGen* gen) { absl::InlinedVector args; args.push_back(std::move(key)); const CommandId* cid = nullptr; if (type == "STRING") { cid = registry.Find("SET"); args.push_back(GenerateValue(val_size, random_value, gen)); } else if (type == "LIST") { cid = registry.Find("LPUSH"); for (uint32_t i = 0; i < elements; ++i) { args.push_back(GenerateValue(val_size, random_value, gen)); } } else if (type == "SET") { cid = registry.Find("SADD"); for (size_t i = 0; i < elements; ++i) { args.push_back(GenerateValue(val_size, random_value, gen)); } } else if (type == "HASH") { cid = registry.Find("HSET"); for (size_t i = 0; i < elements; ++i) { args.push_back(GenerateValue(val_size / 2, random_value, gen)); args.push_back(GenerateValue(val_size / 2, random_value, gen)); } } else if (type == "ZSET") { cid = registry.Find("ZADD"); for (size_t i = 0; i < elements; ++i) { args.push_back(absl::StrCat((*gen)() % val_size)); args.push_back(GenerateValue(val_size, random_value, gen)); } } else if (type == "JSON") { cid = registry.Find("JSON.SET"); args.push_back("$"); string json = "{"; for (size_t i = 0; i < elements; ++i) { absl::StrAppend(&json, "\"", i, "\":\"", GenerateValue(val_size, random_value, gen), "\","); } json[json.size() - 1] = '}'; // Replace last ',' with '}' args.push_back(json); } else if (type == "STREAM") { cid = registry.Find("XADD"); args.push_back("*"); for (size_t i = 0; i < elements; ++i) { args.push_back(GenerateValue(val_size / 2, random_value, gen)); args.push_back(GenerateValue(val_size / 2, random_value, gen)); } } return {cid, args}; } void DoPopulateBatch(string_view type, string_view prefix, size_t val_size, bool random_value, int32_t elements, const PopulateBatch& batch, ServerFamily* sf, ConnectionContext* cntx) { boost::intrusive_ptr local_tx = new Transaction{sf->service().mutable_registry()->Find("EXEC")}; local_tx->StartMultiNonAtomic(); boost::intrusive_ptr stub_tx = new Transaction{local_tx.get(), EngineShard::tlocal()->shard_id(), nullopt}; absl::InlinedVector args_view; facade::CapturingReplyBuilder crb; ConnectionContext local_cntx{cntx, stub_tx.get()}; absl::InsecureBitGen gen; for (unsigned i = 0; i < batch.sz; ++i) { string key = absl::StrCat(prefix, ":", batch.index[i]); auto [cid, args] = GeneratePopulateCommand(type, std::move(key), val_size, random_value, elements, *sf->service().mutable_registry(), &gen); if (!cid) { LOG_EVERY_N(WARNING, 10'000) << "Unable to find command, was it renamed?"; break; } args_view.clear(); for (auto& arg : args) { args_view.push_back(arg); } auto args_span = absl::MakeSpan(args_view); stub_tx->MultiSwitchCmd(cid); local_cntx.cid = cid; crb.SetReplyMode(ReplyMode::NONE); stub_tx->InitByArgs(cntx->ns, local_cntx.conn_state.db_index, args_span); sf->service().InvokeCmd(cid, args_span, &crb, &local_cntx); } local_tx->UnlockMulti(); } struct ObjHist { base::Histogram key_len; base::Histogram val_len; // overall size for the value. base::Histogram card; // for sets, hashmaps etc - it's number of entries. base::Histogram entry_len; // for sets, hashmaps etc - it's the length of each entry. }; // Returns number of O(1) steps executed. unsigned AddObjHist(PrimeIterator it, ObjHist* hist) { using namespace container_utils; const PrimeValue& pv = it->second; size_t val_len = 0; unsigned steps = 1; auto per_entry_cb = [&](ContainerEntry entry) { if (entry.value) { val_len += entry.length; hist->entry_len.Add(entry.length); } else { val_len += 8; // size of long } ++steps; return true; }; hist->key_len.Add(it->first.Size()); if (pv.ObjType() == OBJ_LIST) { IterateList(pv, per_entry_cb, 0, -1); } else if (pv.ObjType() == OBJ_ZSET) { IterateSortedSet(pv.GetRobjWrapper(), [&](ContainerEntry entry, double) { return per_entry_cb(entry); }); } else if (pv.ObjType() == OBJ_SET) { IterateSet(pv, per_entry_cb); } else if (pv.ObjType() == OBJ_HASH) { if (pv.Encoding() == kEncodingListPack) { uint8_t intbuf[LP_INTBUF_SIZE]; uint8_t* lp = (uint8_t*)pv.RObjPtr(); uint8_t* fptr = lpFirst(lp); while (fptr) { size_t entry_len = 0; // field string_view sv = LpGetView(fptr, intbuf); entry_len += sv.size(); // value fptr = lpNext(lp, fptr); entry_len += sv.size(); fptr = lpNext(lp, fptr); hist->entry_len.Add(entry_len); steps += 2; } val_len = lpBytes(lp); } else { StringMap* sm = static_cast(pv.RObjPtr()); for (const auto& k_v : *sm) { hist->entry_len.Add(sdslen(k_v.first) + sdslen(k_v.second) + 2); ++steps; } val_len = sm->ObjMallocUsed() + sm->SetMallocUsed(); } } // TODO: streams if (val_len == 0) { // Fallback val_len = pv.MallocUsed(); } hist->val_len.Add(val_len); if (pv.ObjType() != OBJ_STRING && pv.ObjType() != OBJ_JSON) hist->card.Add(pv.Size()); return steps; } using ObjHistMap = absl::flat_hash_map>; void MergeObjHistMap(ObjHistMap&& src, ObjHistMap* dest) { for (auto& [obj_type, src_hist] : src) { auto& dest_hist = (*dest)[obj_type]; if (!dest_hist) { dest_hist = std::move(src_hist); } else { dest_hist->key_len.Merge(src_hist->key_len); dest_hist->val_len.Merge(src_hist->val_len); dest_hist->card.Merge(src_hist->card); dest_hist->entry_len.Merge(src_hist->entry_len); } } } void DoBuildObjHist(EngineShard* shard, ConnectionContext* cntx, ObjHistMap* obj_hist_map) { auto& db_slice = cntx->ns->GetDbSlice(shard->shard_id()); unsigned steps = 0; for (unsigned i = 0; i < db_slice.db_array_size(); ++i) { DbTable* dbt = db_slice.GetDBTable(i); if (dbt == nullptr) continue; PrimeTable::Cursor cursor; do { cursor = db_slice.Traverse(&dbt->prime, cursor, [&](PrimeIterator it) { unsigned obj_type = it->second.ObjType(); auto& hist_ptr = (*obj_hist_map)[obj_type]; if (!hist_ptr) { hist_ptr.reset(new ObjHist); } steps += AddObjHist(it, hist_ptr.get()); }); if (steps >= 20000) { steps = 0; ThisFiber::Yield(); } } while (cursor); } } ObjInfo InspectOp(ConnectionContext* cntx, string_view key) { auto& db_slice = cntx->ns->GetCurrentDbSlice(); auto db_index = cntx->db_index(); auto [pt, exp_t] = db_slice.GetTables(db_index); PrimeIterator it = pt->Find(key); ObjInfo oinfo; if (IsValid(it)) { const PrimeValue& pv = it->second; oinfo.found = true; oinfo.encoding = pv.Encoding(); oinfo.bucket_id = it.bucket_id(); oinfo.slot_id = it.slot_id(); if (pv.IsExternal()) { oinfo.external_len.emplace(pv.GetExternalSlice().second); } if (pv.HasExpire()) { ExpireIterator exp_it = exp_t->Find(it->first); CHECK(!exp_it.is_done()); time_t exp_time = db_slice.ExpireTime(exp_it); oinfo.ttl = exp_time - GetCurrentTimeMs(); oinfo.has_sec_precision = exp_it->second.is_second_precision(); } } if (!db_slice.CheckLock(IntentLock::EXCLUSIVE, db_index, key)) { oinfo.lock_status = db_slice.CheckLock(IntentLock::SHARED, db_index, key) ? ObjInfo::S : ObjInfo::X; } return oinfo; } OpResult EstimateCompression(ConnectionContext* cntx, string_view key) { auto& db_slice = cntx->ns->GetCurrentDbSlice(); auto db_index = cntx->db_index(); auto [pt, exp_t] = db_slice.GetTables(db_index); PrimeIterator it = pt->Find(key); if (!IsValid(it)) { return OpStatus::KEY_NOTFOUND; } // Only strings are supported right now. if (it->second.ObjType() != OBJ_STRING) { return OpStatus::WRONG_TYPE; } string scratch; string_view value = it->second.GetSlice(&scratch); ValueCompressInfo info; info.raw_size = value.size(); info.compressed_size = info.raw_size; if (info.raw_size >= 32) { size_t compressed_size = ZSTD_compressBound(value.size()); unique_ptr compressed(new char[compressed_size]); info.compressed_size = ZSTD_compress(compressed.get(), compressed_size, value.data(), value.size(), 5); } return info; }; } // namespace DebugCmd::DebugCmd(ServerFamily* owner, cluster::ClusterFamily* cf, ConnectionContext* cntx) : sf_(*owner), cf_(*cf), cntx_(cntx) { } void DebugCmd::Run(CmdArgList args, facade::SinkReplyBuilder* builder) { string subcmd = absl::AsciiStrToUpper(ArgS(args, 0)); if (subcmd == "HELP") { string_view help_arr[] = { "DEBUG [ [value] [opt] ...]. Subcommands are:", "EXEC", " Show the descriptors of the MULTI/EXEC transactions that were processed by ", " the server. For each EXEC/i descriptor, 'i' is the number of shards it touches. ", " Each descriptor details the commands it contained followed by number of their ", " arguments. Each descriptor is prefixed by its frequency count", "OBJECT [COMPRESS]", " Show low-level info about `key` and associated value.", "RELOAD [option ...]", " Save the RDB on disk and reload it back to memory. Valid