Skip to content

Commit ccfd256

Browse files
Vrishal Kulkarnimeta-codesync[bot]
authored andcommitted
Parse string-map options before substitution
Summary: Parse string-map option boundaries before resolving templates so substituted values may contain `:` or `,` without being reinterpreted as delimiters. Preserve each existing caller's grammar with explicit parsing policies: regular `string_map` options reject empty pairs and keep the first duplicate, while `--additional-config-params` skips empty pairs and keeps the last duplicate. Avoid Luna's lossy map-to-string round trip by assigning client `config_params` overrides as typed maps. String-map mismatch diagnostics display the resolved value while retaining the raw Luna value in structured mismatch data. Add regression coverage for Luna, `updateFromDict()`, `compare()`, diagnostics, malformed segments, and duplicate handling. Reviewed By: alikhtarov Differential Revision: D113813215 fbshipit-source-id: c41efb05f45ce0231cb0ee9471c42ac44a558d58
1 parent 0276874 commit ccfd256

4 files changed

Lines changed: 84 additions & 41 deletions

File tree

mcrouter/StandaloneUtils.cpp

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
#include <vector>
2020

2121
#include <folly/Conv.h>
22-
#include <folly/String.h>
2322
#include <folly/system/HardwareConcurrency.h>
2423

2524
#include "mcrouter/CarbonRouterInstance.h"
@@ -492,24 +491,13 @@ void setupStandaloneMcrouter(
492491
// Merge additional-config-params into config_params.
493492
// Additional params take precedence over existing config_params.
494493
if (!cmdLineOpts.additionalConfigParams.empty()) {
495-
auto substituted =
496-
options::substituteTemplates(cmdLineOpts.additionalConfigParams);
497-
std::vector<folly::StringPiece> pairs;
498-
folly::split(',', substituted, pairs);
499-
for (const auto& pair : pairs) {
500-
if (pair.empty()) {
501-
continue;
502-
}
503-
std::string key;
504-
std::string value;
505-
checkLogic(
506-
folly::split(':', pair, key, value),
507-
"Invalid additional-config-params pair: '{}'. Expected name:value.",
508-
pair);
509-
checkLogic(
510-
!key.empty(),
511-
"Empty key in additional-config-params pair: '{}'.",
512-
pair);
494+
auto additionalConfigParams = options::parseStringMapOption(
495+
cmdLineOpts.additionalConfigParams,
496+
{
497+
.ignoreEmptyPairs = true,
498+
.overwriteDuplicateKeys = true,
499+
});
500+
for (auto& [key, value] : additionalConfigParams) {
513501
libmcrouterOptions.config_params[key] = std::move(value);
514502
}
515503
}

mcrouter/options.cpp

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,45 @@ using std::unordered_map;
2525
using std::unordered_set;
2626
using std::vector;
2727

28+
namespace facebook {
29+
namespace memcache {
30+
namespace options {
31+
32+
unordered_map<string, string> parseStringMapOption(
33+
const string& str,
34+
StringMapParseOptions parseOptions) {
35+
vector<folly::StringPiece> pairs;
36+
folly::split(',', str, pairs);
37+
unordered_map<string, string> result;
38+
for (const auto& pair : pairs) {
39+
if (pair.empty()) {
40+
checkLogic(
41+
parseOptions.ignoreEmptyPairs,
42+
"Invalid string map pair: '{}'. Expected name:value.",
43+
pair);
44+
continue;
45+
}
46+
const auto delimiter = pair.find(':');
47+
checkLogic(
48+
delimiter != string::npos,
49+
"Invalid string map pair: '{}'. Expected name:value.",
50+
pair);
51+
auto key = substituteTemplates(pair.subpiece(0, delimiter).str());
52+
checkLogic(!key.empty(), "Empty key in string map pair: '{}'.", pair);
53+
auto value = substituteTemplates(pair.subpiece(delimiter + 1).str());
54+
if (parseOptions.overwriteDuplicateKeys) {
55+
result.insert_or_assign(std::move(key), std::move(value));
56+
} else {
57+
result.emplace(std::move(key), std::move(value));
58+
}
59+
}
60+
return result;
61+
}
62+
63+
} // namespace options
64+
} // namespace memcache
65+
} // namespace facebook
66+
2867
namespace folly {
2968

3069
namespace {
@@ -83,19 +122,7 @@ typename std::enable_if<
83122
std::is_same<unordered_map<string, string>, Tgt>::value,
84123
unordered_map<string, string>>::type
85124
to(const string& s) {
86-
vector<folly::StringPiece> pairs;
87-
folly::split(',', s, pairs);
88-
unordered_map<string, string> result;
89-
for (const auto& it : pairs) {
90-
string key;
91-
string value;
92-
facebook::memcache::checkLogic(
93-
folly::split<false>(':', it, key, value) && !key.empty(),
94-
"Invalid string map pair: '{}'. Expected name:value.",
95-
it);
96-
result.emplace(std::move(key), std::move(value));
97-
}
98-
return result;
125+
return facebook::memcache::options::parseStringMapOption(s);
99126
}
100127

101128
} // namespace folly
@@ -200,20 +227,25 @@ vector<McrouterOptionError> McrouterOptionsBase::updateFromDict(
200227
const boost::any& value) {
201228
auto it = new_opts.find(name);
202229
if (it != new_opts.end()) {
203-
auto subValue = options::substituteTemplates(it->second);
230+
std::optional<string> substitutedValue;
231+
const string* subValue = &it->second;
232+
if (type != McrouterOptionData::Type::string_map) {
233+
substitutedValue = options::substituteTemplates(it->second);
234+
subValue = &*substitutedValue;
235+
}
204236
try {
205-
fromString(subValue, value);
237+
fromString(*subValue, value);
206238
} catch (const std::exception& ex) {
207239
McrouterOptionError e;
208240
e.requestedName = name;
209-
e.requestedValue = subValue;
241+
e.requestedValue = *subValue;
210242
e.errorMsg = "couldn't convert value to " + optionTypeToString(type) +
211243
". Exception: " + ex.what();
212244
errors.push_back(std::move(e));
213245
} catch (...) {
214246
McrouterOptionError e;
215247
e.requestedName = name;
216-
e.requestedValue = subValue;
248+
e.requestedValue = *subValue;
217249
e.errorMsg = "couldn't convert value to " + optionTypeToString(type);
218250
errors.push_back(std::move(e));
219251
}
@@ -252,10 +284,16 @@ vector<McrouterOptionMismatch> McrouterOptionsBase::compare(
252284
"Not found in luna_opts, with type " + optionTypeToString(type);
253285
errors.push_back(std::move(e));
254286
} else {
255-
auto subValue = options::substituteTemplates(it->second);
287+
std::optional<string> substitutedValue;
288+
const string* subValue = &it->second;
289+
if (type != McrouterOptionData::Type::string_map) {
290+
substitutedValue = options::substituteTemplates(it->second);
291+
subValue = &*substitutedValue;
292+
}
256293

257294
try {
258295
auto currValue = toString(value);
296+
auto displayedNewValue = *subValue;
259297
// config_params is a string of comma-separated key-value pairs
260298
// e.g. "key1:value1,key2:value2", and are stored as
261299
// McrouterOptionData::Type::string_map. We need to compare as maps
@@ -269,17 +307,18 @@ vector<McrouterOptionMismatch> McrouterOptionsBase::compare(
269307
if (oldValuePtr == nullptr) {
270308
throw std::runtime_error("could not cast config_params to a map");
271309
}
272-
fromString(subValue, newValue);
310+
fromString(*subValue, newValue);
311+
displayedNewValue = folly::to<string>(newValueMap);
273312
isSame = **boost::any_cast<unordered_map<string, string>*>(
274313
&newValue) == **oldValuePtr;
275314
} else {
276-
isSame = currValue == subValue;
315+
isSame = currValue == *subValue;
277316
}
278317
if (!isSame) {
279318
McrouterOptionMismatch e;
280319
e.optionName = name;
281320
e.lunaValue = it->second;
282-
e.errorMsg = "luna value " + subValue +
321+
e.errorMsg = "luna value " + displayedNewValue +
283322
" and current mcrouter_options value " + currValue +
284323
" are different";
285324
errors.push_back(std::move(e));

mcrouter/options.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,20 @@ class McrouterOptionsBase {
104104

105105
namespace options {
106106

107+
struct StringMapParseOptions {
108+
bool ignoreEmptyPairs{false};
109+
bool overwriteDuplicateKeys{false};
110+
};
111+
107112
/**
108113
* Perform %..% variable substitution on an individual string
109114
*/
110115
std::string substituteTemplates(std::string str);
111116

117+
std::unordered_map<std::string, std::string> parseStringMapOption(
118+
const std::string& str,
119+
StringMapParseOptions parseOptions = {});
120+
112121
} // namespace options
113122
} // namespace memcache
114123
} // namespace facebook

mcrouter/test/cpp_unit_tests/options_test.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,14 @@ TEST(OptionsSetFromDictTest, StringMapValueMayContainColons) {
7070
}
7171

7272
TEST(OptionsSetFromDictTest, StringMapRejectsMalformedPairs) {
73-
for (const auto& value : {"missing-delimiter", ":missing-name"}) {
73+
for (const auto& value : {
74+
"missing-delimiter",
75+
":missing-name",
76+
",a:1",
77+
"a:1,",
78+
"a:1,,b:2",
79+
",",
80+
}) {
7481
McrouterOptions opts;
7582
opts.config_params = {{"existing", "value"}};
7683
const unordered_map<string, string> dict{{"config_params", value}};

0 commit comments

Comments
 (0)