From 91c299b33e1a9ca975cea0fa5bb2dbb25cb214ae Mon Sep 17 00:00:00 2001 From: Roman Gershman Date: Mon, 26 Feb 2024 16:06:21 +0200 Subject: [PATCH] chore: Del and NUMINCRBY use json::Path (#2655) * chore: Del and NUMINCRBY use json::Path Also, fix various protocol bugs when we sent simple string instead of sending bulk strings. Fixed a typo in path.cc that lead to a data race bug. Finally, flip the flag in regression tests to start covering json::Path code and added test coverage for the data race bug --------- Signed-off-by: Roman Gershman --- src/core/json/path.cc | 13 +- src/core/json/path.h | 4 +- src/server/json_family.cc | 319 ++++++++++++++++++--------------- src/server/json_family_test.cc | 11 +- tests/dragonfly/instance.py | 6 +- 5 files changed, 201 insertions(+), 152 deletions(-) diff --git a/src/core/json/path.cc b/src/core/json/path.cc index bf0e33537a19..a1090aa3ff69 100644 --- a/src/core/json/path.cc +++ b/src/core/json/path.cc @@ -365,8 +365,9 @@ auto Dfs::MutateStep(const PathSegment& segment, const MutateCallback& cb, JsonT if (segment.index() >= node->size()) { return make_unexpected(OUT_OF_BOUNDS); } - if (Mutate(cb, nullopt, &node[segment.index()])) { - node->erase(node->array_range().begin() + segment.index()); + auto it = node->array_range().begin() + segment.index(); + if (Mutate(cb, nullopt, &*it)) { + node->erase(it); } } break; @@ -475,10 +476,12 @@ nonstd::expected ParsePath(string_view path) { return driver.TakePath(); } -void MutatePath(const Path& path, MutateCallback callback, JsonType* json) { +unsigned MutatePath(const Path& path, MutateCallback callback, JsonType* json) { if (path.empty()) - return; - Dfs().Mutate(path, callback, json); + return 0; + Dfs dfs; + dfs.Mutate(path, callback, json); + return dfs.matches(); } } // namespace dfly::json diff --git a/src/core/json/path.h b/src/core/json/path.h index d051bc165cd0..b3e8e3ec1e95 100644 --- a/src/core/json/path.h +++ b/src/core/json/path.h @@ -95,7 +95,9 @@ using PathCallback = absl::FunctionRef, con using MutateCallback = absl::FunctionRef, JsonType*)>; void EvaluatePath(const Path& path, const JsonType& json, PathCallback callback); -void MutatePath(const Path& path, MutateCallback callback, JsonType* json); + +// returns number of matches found with the given path. +unsigned MutatePath(const Path& path, MutateCallback callback, JsonType* json); nonstd::expected ParsePath(std::string_view path); } // namespace dfly::json diff --git a/src/server/json_family.cc b/src/server/json_family.cc index dbc4fb5dd64d..5cd81e1ccdf0 100644 --- a/src/server/json_family.cc +++ b/src/server/json_family.cc @@ -43,8 +43,7 @@ using OptBool = optional; using OptLong = optional; using OptSizeT = optional; using OptString = optional; -using JsonReplaceCb = function; -using JsonReplaceVerify = std::function; +using JsonReplaceVerify = std::function; using CI = CommandId; static const char DefaultJsonPath[] = "$"; @@ -75,10 +74,6 @@ inline JsonType Evaluate(const json::Path& expr, const JsonType& obj) { return res; } -inline OpStatus JsonReplaceVerifyNoOp(JsonType&) { - return OpStatus::OK; -} - facade::OpStatus SetJson(const OpArgs& op_args, string_view key, JsonType&& value) { auto& db_slice = op_args.shard->db_slice(); @@ -156,7 +151,7 @@ void PrintOptVec(ConnectionContext* cntx, const OpResult>>& r } } -error_code JsonReplace(JsonType& instance, string_view path, JsonReplaceCb callback) { +error_code JsonReplace(JsonType& instance, string_view path, json::MutateCallback callback) { using evaluator_t = jsonpath::detail::jsonpath_evaluator; using value_type = evaluator_t::value_type; using reference = evaluator_t::reference; @@ -173,8 +168,9 @@ error_code JsonReplace(JsonType& instance, string_view path, JsonReplaceCb callb } jsonpath::detail::dynamic_resources resources; - auto f = [&callback](const json_selector_t::path_node_type& path, reference val) { - callback(path, val); + + auto f = [&callback](const jsonpath::basic_path_node& path, JsonType& val) { + callback(jsonpath::to_string(path), &val); }; expr.evaluate(resources, instance, json_selector_t::path_node_type{}, instance, f, @@ -184,7 +180,7 @@ error_code JsonReplace(JsonType& instance, string_view path, JsonReplaceCb callb // jsoncons version OpStatus UpdateEntry(const OpArgs& op_args, std::string_view key, std::string_view path, - JsonReplaceCb callback, JsonReplaceVerify verify_op = JsonReplaceVerifyNoOp) { + json::MutateCallback callback, JsonReplaceVerify verify_op = {}) { auto it_res = op_args.shard->db_slice().FindMutable(op_args.db_cntx, key, OBJ_JSON); if (!it_res.ok()) { return it_res.status(); @@ -206,13 +202,14 @@ OpStatus UpdateEntry(const OpArgs& op_args, std::string_view key, std::string_vi } // Make sure that we don't have other internal issue with the operation - OpStatus res = verify_op(json_entry); - if (res == OpStatus::OK) { - it_res->post_updater.Run(); - op_args.shard->search_indices()->AddDoc(key, op_args.db_cntx, entry_it->second); + if (verify_op) { + verify_op(json_entry); } - return res; + it_res->post_updater.Run(); + op_args.shard->search_indices()->AddDoc(key, op_args.db_cntx, entry_it->second); + + return OpStatus::OK; } // json::Path version. @@ -428,13 +425,13 @@ void SendJsonValue(RedisReplyBuilder* rb, const JsonType& j) { } else if (j.is_null()) { rb->SendNull(); } else if (j.is_string()) { - rb->SendSimpleString(j.as_string_view()); + rb->SendBulkString(j.as_string_view()); } else if (j.is_object()) { rb->StartArray(j.size() + 1); rb->SendSimpleString("{"); for (const auto& item : j.object_range()) { rb->StartArray(2); - rb->SendSimpleString(item.key()); + rb->SendBulkString(item.key()); SendJsonValue(rb, item.value()); } } else if (j.is_array()) { @@ -587,30 +584,21 @@ OpResult> OpToggle(const OpArgs& op_args, string_view key, strin JsonPathV2 expression) { vector vec; OpStatus status; - if (std::holds_alternative(expression)) { + auto cb = [&vec](optional, JsonType* val) { + if (val->is_bool()) { + bool next_val = val->as_bool() ^ true; + *val = next_val; + vec.emplace_back(next_val); + } else { + vec.emplace_back(nullopt); + } + return false; + }; + + if (holds_alternative(expression)) { const json::Path& expr = std::get(expression); - auto cb = [&vec](optional, JsonType* val) { - if (val->is_bool()) { - bool next_val = val->as_bool() ^ true; - *val = next_val; - vec.emplace_back(next_val); - } else { - vec.emplace_back(nullopt); - } - return false; - }; status = UpdateEntry(op_args, key, expr, cb); } else { - auto cb = [&vec](const auto&, JsonType& val) { - if (val.is_bool()) { - bool current_val = val.as_bool() ^ true; - val = current_val; - vec.emplace_back(current_val); - } else { - vec.emplace_back(nullopt); - } - }; - status = UpdateEntry(op_args, key, path, cb); } if (status != OpStatus::OK) { @@ -619,41 +607,65 @@ OpResult> OpToggle(const OpArgs& op_args, string_view key, strin return vec; } -template +enum ArithmeticOpType { OP_ADD, OP_MULTIPLY }; + +void BinOpApply(double num, bool num_is_double, ArithmeticOpType op, JsonType* val, + bool* overflow) { + double result = 0; + switch (op) { + case OP_ADD: + result = val->as() + num; + break; + case OP_MULTIPLY: + result = val->as() * num; + break; + } + + if (isinf(result)) { + *overflow = true; + return; + } + + if (val->is_double() || num_is_double) { + *val = result; + } else { + *val = (uint64_t)result; + } + *overflow = false; +} + OpResult OpDoubleArithmetic(const OpArgs& op_args, string_view key, string_view path, - double num, Op arithmetic_op) { + double num, ArithmeticOpType op_type, JsonPathV2 expression) { bool is_result_overflow = false; double int_part; bool has_fractional_part = (modf(num, &int_part) != 0); JsonType output(json_array_arg); + OpStatus status; - auto cb = [&](const auto&, JsonType& val) { - if (val.is_number()) { - double result = arithmetic_op(val.as(), num); - if (isinf(result)) { + auto cb = [&](optional, JsonType* val) { + if (val->is_number()) { + bool res = false; + BinOpApply(num, has_fractional_part, op_type, val, &res); + if (res) { is_result_overflow = true; - return; - } - - if (val.is_double() || has_fractional_part) { - val = result; } else { - val = (uint64_t)result; + output.push_back(*val); } - output.push_back(val); } else { output.push_back(JsonType::null()); } + return false; }; - auto verifier = [&is_result_overflow](JsonType&) { - if (is_result_overflow) { - return OpStatus::INVALID_NUMERIC_RESULT; - } - return OpStatus::OK; - }; + if (holds_alternative(expression)) { + const json::Path& path = std::get(expression); + status = UpdateEntry(op_args, key, path, std::move(cb)); + } else { + status = UpdateEntry(op_args, key, path, std::move(cb)); + } - OpStatus status = UpdateEntry(op_args, key, path, cb, verifier); + if (is_result_overflow) + return OpStatus::INVALID_NUMERIC_RESULT; if (status != OpStatus::OK) { return status; } @@ -661,36 +673,46 @@ OpResult OpDoubleArithmetic(const OpArgs& op_args, string_view key, stri return output.as_string(); } -OpResult OpDel(const OpArgs& op_args, string_view key, string_view path) { - long total_deletions = 0; - if (path.empty()) { +// If expression is nullopt, then the whole key should be deleted, otherwise deletes +// items specified by the expression/path. +OpResult OpDel(const OpArgs& op_args, string_view key, string_view path, + optional expression) { + if (!expression || path.empty()) { auto& db_slice = op_args.shard->db_slice(); auto it = db_slice.FindMutable(op_args.db_cntx, key).it; // post_updater will run immediately - total_deletions += long(db_slice.Del(op_args.db_cntx.db_index, it)); - return total_deletions; + return long(db_slice.Del(op_args.db_cntx.db_index, it)); } OpResult result = GetJson(op_args, key); if (!result) { - return total_deletions; + return 0; + } + + if (holds_alternative(*expression)) { + const json::Path& path = get(*expression); + long deletions = json::MutatePath( + path, [](optional, JsonType* val) { return true; }, *result); + return deletions; } vector deletion_items; - auto cb = [&](const JsonExpression::path_node_type& path, JsonType& val) { - deletion_items.emplace_back(jsonpath::to_string(path)); + auto cb = [&](const auto& path, JsonType* val) { + deletion_items.emplace_back(*path); + return false; }; JsonType& json_entry = *(result.value()); - error_code ec = JsonReplace(json_entry, path, cb); + error_code ec = JsonReplace(json_entry, path, std::move(cb)); if (ec) { VLOG(1) << "Failed to evaluate expression on json with error: " << ec.message(); - return total_deletions; + return 0; } if (deletion_items.empty()) { - return total_deletions; + return 0; } + long total_deletions = 0; JsonType patch(json_array_arg, {}); reverse(deletion_items.begin(), deletion_items.end()); // deletion should finish at root keys. for (const auto& item : deletion_items) { @@ -744,18 +766,19 @@ OpResult> OpObjKeys(const OpArgs& op_args, string_view key, OpResult> OpStrAppend(const OpArgs& op_args, string_view key, string_view path, const vector& strs) { vector vec; - auto cb = [&](const auto&, JsonType& val) { - if (val.is_string()) { - string new_val = val.as_string(); + auto cb = [&](const auto&, JsonType* val) { + if (val->is_string()) { + string new_val = val->as_string(); for (auto& str : strs) { new_val += str; } - val = new_val; + *val = new_val; vec.emplace_back(new_val.size()); } else { vec.emplace_back(nullopt); } + return false; }; OpStatus status = UpdateEntry(op_args, key, path, cb); @@ -770,20 +793,21 @@ OpResult> OpStrAppend(const OpArgs& op_args, string_view key, s // Clears containers(arrays or objects) and zeroing numbers. OpResult OpClear(const OpArgs& op_args, string_view key, string_view path) { long clear_items = 0; - auto cb = [&clear_items](const auto& path, JsonType& val) { - if (!(val.is_object() || val.is_array() || val.is_number())) { - return; + auto cb = [&clear_items](const auto& path, JsonType* val) { + if (!(val->is_object() || val->is_array() || val->is_number())) { + return false; } - if (val.is_object()) { - val.erase(val.object_range().begin(), val.object_range().end()); - } else if (val.is_array()) { - val.erase(val.array_range().begin(), val.array_range().end()); - } else if (val.is_number()) { - val = 0; + if (val->is_object()) { + val->erase(val->object_range().begin(), val->object_range().end()); + } else if (val->is_array()) { + val->erase(val->array_range().begin(), val->array_range().end()); + } else if (val->is_number()) { + *val = 0; } clear_items += 1; + return false; }; OpStatus status = UpdateEntry(op_args, key, path, cb); @@ -832,19 +856,15 @@ OpResult> OpArrPop(const OpArgs& op_args, string_view key, str int index, JsonPathV2 expression) { vector vec; OpStatus status; - if (std::holds_alternative(expression)) { + auto cb = [&vec, index](optional, JsonType* val) { + ArrayPop(nullopt, index, val, &vec); + return false; + }; + + if (holds_alternative(expression)) { const json::Path& json_path = std::get(expression); - auto cb = [&vec, index](optional, JsonType* val) { - ArrayPop(nullopt, index, val, &vec); - return false; - }; status = UpdateEntry(op_args, key, json_path, std::move(cb)); } else { - auto cb = [&](const auto& path, JsonType& val) { - ArrayPop(nullopt, index, &val, &vec); - return false; - }; - status = UpdateEntry(op_args, key, path, cb); } if (status != OpStatus::OK) { @@ -857,15 +877,15 @@ OpResult> OpArrPop(const OpArgs& op_args, string_view key, str OpResult> OpArrTrim(const OpArgs& op_args, string_view key, string_view path, int start_index, int stop_index) { vector vec; - auto cb = [&](const auto&, JsonType& val) { - if (!val.is_array()) { + auto cb = [&](const auto&, JsonType* val) { + if (!val->is_array()) { vec.emplace_back(nullopt); - return; + return false; } - if (val.empty()) { + if (val->empty()) { vec.emplace_back(0); - return; + return false; } size_t trim_start_index; @@ -876,26 +896,27 @@ OpResult> OpArrTrim(const OpArgs& op_args, string_view key, str } size_t trim_end_index; - if ((size_t)stop_index >= val.size()) { - trim_end_index = val.size(); + if ((size_t)stop_index >= val->size()) { + trim_end_index = val->size(); } else { trim_end_index = stop_index; } - if (trim_start_index >= val.size() || trim_start_index > trim_end_index) { - val.erase(val.array_range().begin(), val.array_range().end()); + if (trim_start_index >= val->size() || trim_start_index > trim_end_index) { + val->erase(val->array_range().begin(), val->array_range().end()); vec.emplace_back(0); - return; + return false; } - auto trim_start_it = std::next(val.array_range().begin(), trim_start_index); - auto trim_end_it = val.array_range().end(); - if (trim_end_index < val.size()) { - trim_end_it = std::next(val.array_range().begin(), trim_end_index + 1); + auto trim_start_it = std::next(val->array_range().begin(), trim_start_index); + auto trim_end_it = val->array_range().end(); + if (trim_end_index < val->size()) { + trim_end_it = std::next(val->array_range().begin(), trim_end_index + 1); } - val = json_array(trim_start_it, trim_end_it); - vec.emplace_back(val.size()); + *val = json_array(trim_start_it, trim_end_it); + vec.emplace_back(val->size()); + return false; }; OpStatus status = UpdateEntry(op_args, key, path, cb); @@ -913,46 +934,47 @@ OpResult> OpArrInsert(const OpArgs& op_args, string_view key, s // Insert user-supplied value into the supplied index that should be valid. // If at least one index isn't valid within an array in the json doc, the operation is discarded. // Negative indexes start from the end of the array. - auto cb = [&](const auto&, JsonType& val) { + auto cb = [&](const auto&, JsonType* val) { if (out_of_boundaries_encountered) { - return; + return false; } - if (!val.is_array()) { + if (!val->is_array()) { vec.emplace_back(nullopt); - return; + return false; } size_t removal_index; if (index < 0) { - if (val.empty()) { + if (val->empty()) { out_of_boundaries_encountered = true; - return; + return false; } - int temp_index = index + val.size(); + int temp_index = index + val->size(); if (temp_index < 0) { out_of_boundaries_encountered = true; - return; + return false; } removal_index = temp_index; } else { - if ((size_t)index > val.size()) { + if ((size_t)index > val->size()) { out_of_boundaries_encountered = true; - return; + return false; } removal_index = index; } - auto it = next(val.array_range().begin(), removal_index); + auto it = next(val->array_range().begin(), removal_index); for (auto& new_val : new_values) { - it = val.insert(it, new_val); + it = val->insert(it, new_val); it++; } - vec.emplace_back(val.size()); + vec.emplace_back(val->size()); + return false; }; OpStatus status = UpdateEntry(op_args, key, path, cb); @@ -978,18 +1000,19 @@ OpResult> OpArrAppend(const OpArgs& op_args, string_view key, s return result.status(); } - auto cb = [&](const auto&, JsonType& val) { - if (!val.is_array()) { + auto cb = [&](const auto&, JsonType* val) { + if (!val->is_array()) { vec.emplace_back(nullopt); - return; + return false; } for (auto& new_val : append_values) { - val.emplace_back(new_val); + val->emplace_back(new_val); } - vec.emplace_back(val.size()); + vec.emplace_back(val->size()); + return false; }; - OpStatus status = UpdateEntry(op_args, key, path, cb); + OpStatus status = UpdateEntry(op_args, key, path, std::move(cb)); if (status != OpStatus::OK) { return status; } @@ -1194,12 +1217,13 @@ OpResult OpSet(const OpArgs& op_args, string_view key, string_view path, bool path_exists = false; bool operation_result = false; const JsonType& new_json = parsed_json.value(); - auto cb = [&](const auto&, JsonType& val) { + auto cb = [&](const auto&, JsonType* val) { path_exists = true; if (!is_nx_condition) { operation_result = true; - val = new_json; + *val = new_json; } + return false; }; auto inserter = [&](JsonType& json) { @@ -1287,7 +1311,7 @@ void JsonFamily::Set(CmdArgList args, ConnectionContext* cntx) { auto* rb = static_cast(cntx->reply_builder()); if (result) { if (*result) { - rb->SendSimpleString("OK"); + rb->SendOk(); } else { rb->SendNull(); } @@ -1331,9 +1355,9 @@ void JsonFamily::Debug(CmdArgList args, ConnectionContext* cntx) { if (absl::EqualsIgnoreCase(command, "help")) { auto* rb = static_cast(cntx->reply_builder()); rb->StartArray(2); - rb->SendSimpleString( + rb->SendBulkString( "JSON.DEBUG FIELDS - report number of fields in the JSON element."); - rb->SendSimpleString("JSON.DEBUG HELP - print help message."); + rb->SendBulkString("JSON.DEBUG HELP - print help message."); return; } else if (absl::EqualsIgnoreCase(command, "fields")) { @@ -1669,12 +1693,16 @@ void JsonFamily::ObjKeys(CmdArgList args, ConnectionContext* cntx) { void JsonFamily::Del(CmdArgList args, ConnectionContext* cntx) { string_view key = ArgS(args, 0); string_view path; + + optional expression; + if (args.size() > 1) { path = ArgS(args, 1); + expression.emplace(PARSE_PATHV2(path)); } auto cb = [&](Transaction* t, EngineShard* shard) { - return OpDel(t->GetOpArgs(shard), key, path); + return OpDel(t->GetOpArgs(shard), key, path, std::move(expression)); }; Transaction* trans = cntx->transaction; @@ -1693,17 +1721,19 @@ void JsonFamily::NumIncrBy(CmdArgList args, ConnectionContext* cntx) { return; } + JsonPathV2 expression = PARSE_PATHV2(path); + auto cb = [&](Transaction* t, EngineShard* shard) { - return OpDoubleArithmetic(t->GetOpArgs(shard), key, path, dnum, plus{}); + return OpDoubleArithmetic(t->GetOpArgs(shard), key, path, dnum, OP_ADD, std::move(expression)); }; - Transaction* trans = cntx->transaction; - OpResult result = trans->ScheduleSingleHopT(std::move(cb)); + OpResult result = cntx->transaction->ScheduleSingleHopT(std::move(cb)); + auto* rb = static_cast(cntx->reply_builder()); if (result) { - cntx->SendSimpleString(*result); + rb->SendBulkString(*result); } else { - cntx->SendError(result.status()); + rb->SendError(result.status()); } } @@ -1718,17 +1748,20 @@ void JsonFamily::NumMultBy(CmdArgList args, ConnectionContext* cntx) { return; } + JsonPathV2 expression = PARSE_PATHV2(path); + auto cb = [&](Transaction* t, EngineShard* shard) { - return OpDoubleArithmetic(t->GetOpArgs(shard), key, path, dnum, multiplies{}); + return OpDoubleArithmetic(t->GetOpArgs(shard), key, path, dnum, OP_MULTIPLY, + std::move(expression)); }; - Transaction* trans = cntx->transaction; - OpResult result = trans->ScheduleSingleHopT(std::move(cb)); + auto* rb = static_cast(cntx->reply_builder()); + OpResult result = cntx->transaction->ScheduleSingleHopT(std::move(cb)); if (result) { - cntx->SendSimpleString(*result); + rb->SendBulkString(*result); } else { - cntx->SendError(result.status()); + rb->SendError(result.status()); } } diff --git a/src/server/json_family_test.cc b/src/server/json_family_test.cc index f3a96232a192..a4eb41db9d42 100644 --- a/src/server/json_family_test.cc +++ b/src/server/json_family_test.cc @@ -363,8 +363,11 @@ TEST_F(JsonFamilyTest, NumIncrBy) { resp = Run({"JSON.NUMINCRBY", "json", "$.d[*]", "1"}); EXPECT_EQ(resp, "[2,3,4]"); + resp = Run({"JSON.NUMINCRBY", "json", "$.d[2]", "1"}); + EXPECT_EQ(resp, "[5]"); + resp = Run({"JSON.GET", "json", "$.*"}); - EXPECT_EQ(resp, R"([[],[2],[2,3],[2,3,4]])"); + EXPECT_EQ(resp, R"([[],[2],[2,3],[2,3,5]])"); json = R"( {"a":{}, "b":{"a":1}, "c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}} @@ -513,7 +516,11 @@ TEST_F(JsonFamilyTest, Del) { EXPECT_EQ(resp, R"({"a":{},"b":{"a":1},"c":{"a":1,"b":2},"d":{},"e":[]})"); resp = Run({"JSON.DEL", "json", "$..*"}); - EXPECT_THAT(resp, IntArg(8)); + + // TODO: legacy jsoncons implementation returns, 8 but in practive it should return 5. + // redis-stack returns 5 as well. + // Once we drop jsoncons path, we can enforce here equality. + EXPECT_GE(resp.GetInt(), 5); resp = Run({"JSON.GET", "json"}); EXPECT_EQ(resp, R"({})"); diff --git a/tests/dragonfly/instance.py b/tests/dragonfly/instance.py index 0e69f59859bf..47465f999670 100644 --- a/tests/dragonfly/instance.py +++ b/tests/dragonfly/instance.py @@ -1,4 +1,5 @@ import dataclasses +import os import time import subprocess import aiohttp @@ -190,7 +191,8 @@ def _start(self): self._port = None all_args = self.format_args(self.args) - logging.debug(f"Starting instance with arguments {all_args} from {self.params.path}") + real_path = os.path.realpath(self.params.path) + logging.debug(f"Starting instance with arguments {' '.join(all_args)} from {real_path}") run_cmd = [self.params.path, *all_args] if self.params.gdb: @@ -315,6 +317,8 @@ def __init__(self, params: DflyParams, args): def create(self, existing_port=None, **kwargs) -> DflyInstance: args = {**self.args, **kwargs} args.setdefault("dbfilename", "") + args.setdefault("jsonpathv2", None) + vmod = "dragonfly_connection=1,accept_server=1,listener_interface=1,main_service=1,rdb_save=1,replica=1,cluster_family=1" args.setdefault("vmodule", vmod)