fix(json_family): Fix memory tracking for the JSON.DEL command

fixes dragonflydb#5055

Signed-off-by: Stepan Bagritsevich <stefan@dragonflydb.io>
This commit is contained in:
Stepan Bagritsevich 2025-05-06 17:31:32 +02:00
parent 91ef2af169
commit e9976f1df6
No known key found for this signature in database
GPG key ID: 92DAADC9F15EE26E
3 changed files with 144 additions and 42 deletions

View file

@ -54,34 +54,6 @@ using CI = CommandId;
namespace { namespace {
class JsonMemTracker {
public:
JsonMemTracker() {
start_size_ = static_cast<MiMemoryResource*>(CompactObj::memory_resource())->used();
}
void SetJsonSize(PrimeValue& pv, bool is_op_set) {
const size_t current = static_cast<MiMemoryResource*>(CompactObj::memory_resource())->used();
int64_t diff = static_cast<int64_t>(current) - static_cast<int64_t>(start_size_);
// If the diff is 0 it means the object use the same memory as before. No action needed.
if (diff == 0) {
return;
}
// If op_set_ it means we JSON.SET or JSON.MSET was called. This is a blind update,
// and because the operation sets the size to 0 we also need to include the size of
// the pointer.
if (is_op_set) {
diff += static_cast<int64_t>(mi_usable_size(pv.GetJson()));
}
pv.SetJsonSize(diff);
// Under any flow we must not end up with this special value.
DCHECK(pv.MallocUsed() != 0);
}
private:
size_t start_size_{0};
};
/* Helper class which must be initialized before any mutate operations on json. /* Helper class which must be initialized before any mutate operations on json.
It will track the memory usage of the json object and update the size in the CompactObj. It will track the memory usage of the json object and update the size in the CompactObj.
It also contains indexes updates, post update operations on the iterator. */ It also contains indexes updates, post update operations on the iterator. */
@ -107,6 +79,8 @@ class JsonAutoUpdater {
void SetJsonSize() { void SetJsonSize() {
set_size_was_called_ = true; set_size_was_called_ = true;
ShrinkJsonIfNeeded();
const size_t current = GetMemoryUsage(); const size_t current = GetMemoryUsage();
int64_t diff = static_cast<int64_t>(current) - static_cast<int64_t>(start_size_); int64_t diff = static_cast<int64_t>(current) - static_cast<int64_t>(start_size_);
@ -145,6 +119,16 @@ class JsonAutoUpdater {
return static_cast<MiMemoryResource*>(CompactObj::memory_resource())->used(); return static_cast<MiMemoryResource*>(CompactObj::memory_resource())->used();
} }
/* Shrinks the json object to fit its current size.
Sometimes after mutating the json object, it may have more capacity than needed.
This method will reduce the capacity to fit the current size. */
void ShrinkJsonIfNeeded() {
auto json = GetJson();
if (json->size() * 2 < json->capacity()) {
json->shrink_to_fit();
}
}
private: private:
const OpArgs& op_args_; const OpArgs& op_args_;
string_view key_; string_view key_;
@ -1060,29 +1044,24 @@ OpResult<long> OpDel(const OpArgs& op_args, string_view key, string_view path,
return 0; return 0;
} }
PrimeValue& pv = it_res->it->second;
JsonType* json_val = pv.GetJson();
JsonMemTracker tracker;
absl::Cleanup update_size_on_exit([tracker, &pv]() mutable { tracker.SetJsonSize(pv, false); });
if (json_path.HoldsJsonPath()) { if (json_path.HoldsJsonPath()) {
JsonAutoUpdater updater(op_args, key, *std::move(it_res), true);
const json::Path& path = json_path.AsJsonPath(); const json::Path& path = json_path.AsJsonPath();
long deletions = json::MutatePath( long deletions = json::MutatePath(
path, [](optional<string_view>, JsonType* val) { return true; }, json_val); path, [](optional<string_view>, JsonType* val) { return true; }, updater.GetJson());
return deletions; return deletions;
} }
// Allocates memory for the deletion_items.
// So we need to initialize JsonAutoUpdater after this callback
vector<string> deletion_items; vector<string> deletion_items;
auto cb = [&](std::optional<std::string_view> path, JsonType* val) -> MutateCallbackResult<> { auto cb = [&deletion_items](string_view path, const JsonType& val) -> Nothing {
deletion_items.emplace_back(*path); deletion_items.emplace_back(path);
return {}; return {};
}; };
auto res = json_path.ExecuteMutateCallback<Nothing>( auto res = json_path.ExecuteReadOnlyCallback<Nothing>(
json_val, std::move(cb), CallbackResultOptions::DefaultMutateOptions()); it_res->it->second.GetJson(), cb, CallbackResultOptions::DefaultReadOnlyOptions());
RETURN_ON_BAD_STATUS(res);
if (deletion_items.empty()) { if (deletion_items.empty()) {
return 0; return 0;
} }
@ -1097,13 +1076,17 @@ OpResult<long> OpDel(const OpArgs& op_args, string_view key, string_view path,
patch.emplace_back(patch_item); patch.emplace_back(patch_item);
} }
JsonAutoUpdater updater(op_args, key, *std::move(it_res));
std::error_code ec; std::error_code ec;
jsoncons::jsonpatch::apply_patch(*json_val, patch, ec); jsoncons::jsonpatch::apply_patch(*updater.GetJson(), patch, ec);
if (ec) { if (ec) {
VLOG(1) << "Failed to apply patch on json with error: " << ec.message(); VLOG(1) << "Failed to apply patch on json with error: " << ec.message();
return 0; return 0;
} }
updater.SetJsonSize();
// SetString(op_args, key, j.as_string()); // SetString(op_args, key, j.as_string());
return total_deletions; return total_deletions;
} }

View file

@ -105,4 +105,108 @@ TEST_F(JsonFamilyMemoryTest, PartialSet) {
EXPECT_THAT(resp, IntArg(start_size)); EXPECT_THAT(resp, IntArg(start_size));
} }
/* Tests how works memory usage after deleting json object in jsoncons */
TEST_F(JsonFamilyMemoryTest, JsonConsDelTest) {
std::string_view start_json = R"({"a":"some text", "b":" "})";
size_t start = GetMemoryUsage();
auto json = dfly::JsonFromString(start_json, JsonFamilyMemoryTest::GetMemoryResource());
void* ptr =
JsonFamilyMemoryTest::GetMemoryResource()->allocate(sizeof(JsonType), alignof(JsonType));
JsonType* json_on_heap = new (ptr) JsonType(std::move(json).value());
size_t memory_usage_before_erase = GetMemoryUsage() - start;
json_on_heap->erase("a");
/* To deallocate memory we should use shrink_to_fit */
json_on_heap->shrink_to_fit();
size_t memory_usage_after_erase = GetMemoryUsage() - start;
EXPECT_GT(memory_usage_before_erase, memory_usage_after_erase);
EXPECT_EQ(memory_usage_after_erase, GetJsonMemoryUsageFromString(R"({"b":" "})"));
}
TEST_F(JsonFamilyMemoryTest, SimpleDel) {
std::string_view start_json = R"({"a":"some text", "b":" "})";
size_t start_size = GetJsonMemoryUsageFromString(start_json);
auto resp = Run({"JSON.SET", "j1", "$", start_json});
EXPECT_EQ(resp, "OK");
resp = GetJsonMemoryUsageFromDb("j1");
EXPECT_THAT(resp, IntArg(start_size));
std::string_view json_after_del = R"({"b":" "})";
size_t size_after_del = GetJsonMemoryUsageFromString(json_after_del);
// Test that raw memory usage is correct
resp = Run({"JSON.SET", "j2", "$", json_after_del});
EXPECT_EQ(resp, "OK");
resp = GetJsonMemoryUsageFromDb("j2");
EXPECT_THAT(resp, IntArg(size_after_del));
// Test that after deletion memory usage is correct
resp = Run({"JSON.DEL", "j1", "$.a"});
EXPECT_THAT(resp, IntArg(1));
resp = Run({"JSON.GET", "j1"});
EXPECT_EQ(resp, json_after_del);
resp = GetJsonMemoryUsageFromDb("j1");
/* We still expect the initial size here, because after deletion we do not call shrink_to_fit on
the JSON object. As a result, the memory will not be deallocated. Check
JsonFamilyMemoryTest::JsonConsDelTest for example. */
EXPECT_THAT(resp, IntArg(start_size));
// Again set start json
resp = Run({"JSON.SET", "j1", "$.a", "\"some text\""});
EXPECT_EQ(resp, "OK");
resp = GetJsonMemoryUsageFromDb("j1");
EXPECT_THAT(resp, IntArg(start_size));
}
TEST_F(JsonFamilyMemoryTest, JsonShrinking) {
std::string_view start_json = R"({"a":"some text","b":"some another text","c":" "})";
size_t start_size = GetJsonMemoryUsageFromString(start_json);
auto resp = Run({"JSON.SET", "j1", "$", start_json});
EXPECT_EQ(resp, "OK");
resp = GetJsonMemoryUsageFromDb("j1");
EXPECT_THAT(resp, IntArg(start_size));
std::string_view json_after_del = R"({"c":" "})";
size_t size_after_del = GetJsonMemoryUsageFromString(json_after_del);
// Test that raw memory usage is correct
resp = Run({"JSON.SET", "j2", "$", json_after_del});
EXPECT_EQ(resp, "OK");
resp = GetJsonMemoryUsageFromDb("j2");
EXPECT_THAT(resp, IntArg(size_after_del));
// Test that after deletion memory usage decreases
resp = Run({"JSON.DEL", "j1", "$.a"});
EXPECT_THAT(resp, IntArg(1));
resp = Run({"JSON.DEL", "j1", "$.b"});
EXPECT_THAT(resp, IntArg(1));
resp = Run({"JSON.GET", "j1"});
EXPECT_EQ(resp, json_after_del);
resp = GetJsonMemoryUsageFromDb("j1");
// Now we expect the size to be smaller, because shrink_to_fit was called
EXPECT_THAT(resp, IntArg(size_after_del));
// Again set start json
resp = Run({"JSON.SET", "j1", "$.a", "\"some text\""});
EXPECT_EQ(resp, "OK");
resp = Run({"JSON.SET", "j1", "$.b", "\"some another text\""});
EXPECT_EQ(resp, "OK");
resp = Run({"JSON.GET", "j1"});
EXPECT_EQ(resp, start_json);
resp = GetJsonMemoryUsageFromDb("j1");
// Jsoncons will allocate more memory for the new json that needed.
// This is totally fine, because we will not call shrink_to_fit.
EXPECT_THAT(resp, IntArg(368));
EXPECT_GT(368, start_size);
}
} // namespace dfly } // namespace dfly

View file

@ -2767,4 +2767,19 @@ TEST_F(SearchFamilyTest, JsonSetIndexesBug) {
resp = Run({"FT.AGGREGATE", "index", "*", "GROUPBY", "1", "@text"}); resp = Run({"FT.AGGREGATE", "index", "*", "GROUPBY", "1", "@text"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("text", "some text"))); EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("text", "some text")));
} }
TEST_F(SearchFamilyTest, JsonDelIndexesBug) {
auto resp = Run({"JSON.SET", "j1", "$", R"({"text":"some text"})"});
EXPECT_THAT(resp, "OK");
resp = Run(
{"FT.CREATE", "index", "ON", "json", "SCHEMA", "$.text", "AS", "text", "TEXT", "SORTABLE"});
EXPECT_THAT(resp, "OK");
resp = Run({"JSON.DEL", "j1", "$.text"});
EXPECT_THAT(resp, IntArg(1));
resp = Run({"FT.AGGREGATE", "index", "*", "GROUPBY", "1", "@text"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("text", ArgType(RespExpr::NIL))));
}
} // namespace dfly } // namespace dfly