Commit 3cec1c6b authored by Anton Likhtarov's avatar Anton Likhtarov Committed by Facebook Github Bot

folly::settings basic functionality

Summary:
A library that provides gflags-like functionality, with the addition of:
- Threadsafe read/writes for all flags
- Arbitrary C++ types
- No declare/define split, all setting definitions are declaration and are safe to place in headers
- Runtime introspection of all flags
- Compile time and runtime collision checks
- Namespaces per project

Down the road:
- Overlay support (change a set of settings based on some context)
- Construct arbitrary user types from string
- Support multiple library instances with distinct settings in a single process
- Callbacks on setting updates

Reviewed By: aary, mnv104

Differential Revision: D6217956

fbshipit-source-id: 7c03d79c7601ce7b266df78e62b7193712d8331e
parent d1d2a9d6
/*
* Copyright 2018-present Facebook, Inc.
*
* Licensed 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 "Settings.h"
#include <map>
#include <folly/Synchronized.h>
namespace folly {
namespace settings {
namespace detail {
namespace {
class Signature {
public:
Signature(
const std::type_info& type,
std::string defaultString,
std::string desc,
bool unique)
: type_(type),
defaultString_(std::move(defaultString)),
desc_(std::move(desc)),
unique_(unique) {}
bool operator==(const Signature& other) const {
/* unique_ field is ignored on purpose */
return type_ == other.type_ && defaultString_ == other.defaultString_ &&
desc_ == other.desc_;
}
bool unique() const {
return unique_;
}
private:
std::type_index type_;
std::string defaultString_;
std::string desc_;
bool unique_;
};
using SettingsMap = std::
map<std::string, std::pair<Signature, std::shared_ptr<SettingCoreBase>>>;
Synchronized<SettingsMap>& settingsMap() {
static Indestructible<Synchronized<SettingsMap>> map;
return *map;
}
} // namespace
std::shared_ptr<SettingCoreBase> registerImpl(
StringPiece project,
StringPiece name,
const std::type_info& type,
StringPiece defaultString,
StringPiece desc,
bool unique,
std::shared_ptr<SettingCoreBase> base) {
if (project.empty() || project.find('_') != std::string::npos) {
throw std::logic_error(
"Setting project must be nonempty and cannot contain underscores: " +
project.str());
}
auto fullname = project.str() + "_" + name.str();
Signature sig(type, defaultString.str(), desc.str(), unique);
auto mapPtr = settingsMap().wlock();
auto it = mapPtr->find(fullname);
if (it != mapPtr->end()) {
if (it->second.first == sig) {
if (unique || it->second.first.unique()) {
throw std::logic_error("FOLLY_SETTING not unique: " + fullname);
}
/* Identical SHARED setting in a different translation unit,
reuse it */
return it->second.second;
}
throw std::logic_error("Setting collision detected: " + fullname);
}
mapPtr->emplace(std::move(fullname), std::make_pair(std::move(sig), base));
return base;
}
} // namespace detail
bool setFromString(
StringPiece settingName,
StringPiece newValue,
StringPiece reason) {
auto mapPtr = detail::settingsMap().rlock();
auto it = mapPtr->find(settingName.str());
if (it == mapPtr->end()) {
return false;
}
it->second.second->setFromString(newValue, reason);
return true;
}
Optional<std::pair<std::string, std::string>> getAsString(
StringPiece settingName) {
auto mapPtr = detail::settingsMap().rlock();
auto it = mapPtr->find(settingName.str());
if (it == mapPtr->end()) {
return folly::none;
}
return it->second.second->getAsString();
}
bool resetToDefault(StringPiece settingName) {
auto mapPtr = detail::settingsMap().rlock();
auto it = mapPtr->find(settingName.str());
if (it == mapPtr->end()) {
return false;
}
it->second.second->resetToDefault();
return true;
}
void forEachSetting(
const std::function<
void(StringPiece, StringPiece, StringPiece, const std::type_info&)>&
func) {
detail::SettingsMap map;
/* Note that this won't hold the lock over the callback, which is
what we want since the user might call other settings:: APIs */
map = *detail::settingsMap().rlock();
for (const auto& kv : map) {
auto value = kv.second.second->getAsString();
func(kv.first, value.first, value.second, kv.second.second->typeId());
}
}
} // namespace settings
} // namespace folly
/*
* Copyright 2018-present Facebook, Inc.
*
* Licensed 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.
*/
#pragma once
#include <functional>
#include <string>
#include <folly/Indestructible.h>
#include <folly/Range.h>
#include <folly/experimental/settings/detail/SettingsImpl.h>
namespace folly {
namespace settings {
namespace detail {
template <class SettingMeta>
class SettingHandle {
public:
/**
* Setting type
*/
using Type = typename SettingMeta::Type;
/**
* Returns the setting's current value.
* Note that the returned reference is not guaranteed to be long-lived
* and should not be saved anywhere.
*/
const Type& operator*() const {
return core().get();
}
const Type& operator->() const {
return core().get();
}
/**
* Atomically updates the setting's current value.
* @param reason Will be stored with the current value, useful for debugging.
*/
static void set(const Type& t, folly::StringPiece reason = "api") {
core().set(t, reason);
}
SettingHandle() {
/* Ensure setting is registered */
core();
}
private:
static SettingCore<SettingMeta>& core() {
static /* library-local */
Indestructible<std::shared_ptr<SettingCoreBase>>
core = registerImpl(
SettingMeta::project(),
SettingMeta::name(),
typeid(typename SettingMeta::Type),
SettingMeta::defaultString(),
SettingMeta::desc(),
SettingMeta::unique(),
std::make_shared<SettingCore<SettingMeta>>());
return static_cast<SettingCore<SettingMeta>&>(**core);
}
};
} // namespace detail
#define FOLLY_SETTING_IMPL(_project, _Type, _name, _def, _desc, _unique) \
namespace { \
struct FOLLY_SETTINGS_META__##_project##_##_name { \
using Type = _Type; \
static folly::StringPiece project() { \
return #_project; \
} \
static folly::StringPiece name() { \
return #_name; \
} \
static Type def() { \
return _def; \
} \
static folly::StringPiece defaultString() { \
return #_def; \
} \
static folly::StringPiece desc() { \
return _desc; \
} \
static bool unique() { \
return _unique; \
} \
}; \
folly::settings::detail::SettingHandle< \
FOLLY_SETTINGS_META__##_project##_##_name> \
SETTING_##_project##_##_name; \
} \
/* hack to require a trailing semicolon */ \
int FOLLY_SETTINGS_IGNORE__##_project##_##_name()
/**
* Defines a setting.
*
* FOLLY_SETTING_SHARED(): syntactically, think of it like a class
* definition. You can place the identical setting definitions in
* distinct translation units, but not in the same translation unit.
* In particular, you can place a setting definition in a header file
* and include it from multiple .cpp files - all of these definitions
* will refer to a single setting.
*
* FOLLY_SETTING() variant can only be placed in a single translation unit
* and will be checked against accidental collisions.
*
* The setting API can be accessed via SETTING_project_name::<api_func>() and
* is documented in the SettingHandle class.
*
* While the specific SETTING_project_name classes are declared
* inplace and are namespace local, all settings for a given project
* share a common namespace and collisions are verified at runtime on
* program startup.
*
* @param _project Project identifier, can only contain [a-zA-Z0-9]
* @param _Type setting value type
* @param _name setting name within the project, can only contain [_a-zA-Z0-9].
* The string "<project>_<name>" must be unique for the whole program.
* @param _def default value for the setting
* @param _desc setting documentation
*/
#define FOLLY_SETTING(_project, _Type, _name, _def, _desc) \
FOLLY_SETTING_IMPL(_project, _Type, _name, _def, _desc, /* unique */ true)
#define FOLLY_SETTING_SHARED(_project, _Type, _name, _def, _desc) \
FOLLY_SETTING_IMPL(_project, _Type, _name, _def, _desc, /* unique */ false)
/**
* Look up a setting by name, and update the value from a string representation.
*
* @returns True if the setting was successfully updated, false if no setting
* with that name was found.
* @throws std::runtime_error If there's a conversion error.
*/
bool setFromString(
folly::StringPiece settingName,
folly::StringPiece newValue,
folly::StringPiece reason);
/**
* @return If the setting exists, the current (to<string>(value),
* reason) pair. Empty Optional otherwise.
*/
folly::Optional<std::pair<std::string, std::string>> getAsString(
folly::StringPiece settingName);
/**
* Reset the value of the setting identified by name to its default value.
* The reason will be set to "default".
*
* @return True if the setting was reset, false if the setting is not found.
*/
bool resetToDefault(folly::StringPiece settingName);
/**
* Iterates over all known settings and calls
* func(name, to<string>(value), reason, typeid(Type)) for each.
*/
void forEachSetting(const std::function<void(
folly::StringPiece,
folly::StringPiece,
folly::StringPiece,
const std::type_info&)>& func);
} // namespace settings
} // namespace folly
/*
* Copyright 2018-present Facebook, Inc.
*
* Licensed 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.
*/
#pragma once
#include <memory>
#include <mutex>
#include <string>
#include <typeindex>
#include <folly/CachelinePadded.h>
#include <folly/Conv.h>
#include <folly/Range.h>
#include <folly/SharedMutex.h>
namespace folly {
namespace settings {
namespace detail {
template <class Type>
struct SettingContents {
std::string updateReason;
Type value;
template <class... Args>
SettingContents(std::string _reason, Args&&... args)
: updateReason(std::move(_reason)), value(std::forward<Args>(args)...) {}
};
class SettingCoreBase {
public:
virtual void setFromString(StringPiece newValue, StringPiece reason) = 0;
virtual std::pair<std::string, std::string> getAsString() const = 0;
virtual void resetToDefault() = 0;
virtual const std::type_info& typeId() const = 0;
virtual ~SettingCoreBase() {}
};
std::shared_ptr<SettingCoreBase> registerImpl(
StringPiece project,
StringPiece name,
const std::type_info& type,
StringPiece defaultString,
StringPiece desc,
bool unique,
std::shared_ptr<SettingCoreBase> base);
template <class SettingMeta>
class SettingCore : public SettingCoreBase {
public:
using Type = typename SettingMeta::Type;
using Contents = SettingContents<Type>;
void setFromString(StringPiece newValue, StringPiece reason) override {
set(to<Type>(newValue), reason.str());
}
std::pair<std::string, std::string> getAsString() const override {
auto contents = *const_cast<SettingCore*>(this)->tlValue();
return std::make_pair(
to<std::string>(contents.value), contents.updateReason);
}
void resetToDefault() override {
set(SettingMeta::def(), "default");
}
const std::type_info& typeId() const override {
return typeid(Type);
}
const Type& get() const {
return const_cast<SettingCore*>(this)->tlValue()->value;
}
void set(const Type& t, StringPiece reason) {
SharedMutex::WriteHolder lg(globalLock_);
globalValue_ = std::make_shared<Contents>(reason.str(), t);
++(*globalVersion_);
}
SettingCore()
: globalValue_(
std::make_shared<Contents>("default", SettingMeta::def())) {}
~SettingCore() {}
private:
SharedMutex globalLock_;
std::shared_ptr<Contents> globalValue_;
/* Local versions start at 0, this will force a read on first local access. */
CachelinePadded<std::atomic<size_t>> globalVersion_{1};
std::shared_ptr<Contents>& tlValue() {
thread_local CachelinePadded<
Indestructible<std::pair<size_t, std::shared_ptr<Contents>>>>
localValue;
auto& value = **localValue;
while (value.first < *globalVersion_) {
/* If this destroys the old value, do it without holding the lock */
value.second.reset();
SharedMutex::ReadHolder lg(globalLock_);
value.first = *globalVersion_;
value.second = globalValue_;
}
return value.second;
}
};
} // namespace detail
} // namespace settings
} // namespace folly
/*
* Copyright 2018-present Facebook, Inc.
*
* Licensed 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 <folly/Benchmark.h>
#include <folly/experimental/settings/Settings.h>
#include <folly/init/Init.h>
/*
============================================================================
folly/experimental/settings/test/SettingsBenchmarks.cpprelative time/iter
iters/s
============================================================================
settings_get_bench 1.73ns 577.36M
============================================================================
*/
FOLLY_SETTING(follytest, int, benchmarked, 100, "desc");
BENCHMARK(settings_get_bench, iters) {
for (unsigned int i = 0; i < iters; ++i) {
auto value = *SETTING_follytest_benchmarked;
folly::doNotOptimizeAway(value);
}
}
int main(int argc, char** argv) {
folly::init(&argc, &argv);
folly::runBenchmarks();
return 0;
}
/*
* Copyright 2018-present Facebook, Inc.
*
* Licensed 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 <folly/experimental/settings/Settings.h>
#include <folly/Format.h>
#include <folly/portability/GTest.h>
#include "a.h"
#include "b.h"
namespace some_ns {
FOLLY_SETTING(follytest, std::string, some_flag, "default", "Description");
FOLLY_SETTING(
follytest,
std::string,
unused,
"unused_default",
"Not used, but should still be in the list");
// Enable to test runtime collision checking logic
#if 0
FOLLY_SETTING(follytest, std::string, internal_flag_to_a, "collision_with_a",
"Collision_with_a");
#endif
} // namespace some_ns
TEST(Settings, basic) {
std::string allFlags;
folly::settings::forEachSetting([&allFlags](
folly::StringPiece name,
folly::StringPiece value,
folly::StringPiece reason,
const std::type_info& type) {
std::string typeName;
if (type == typeid(int)) {
typeName = "int";
} else if (type == typeid(std::string)) {
typeName = "std::string";
} else {
ASSERT_FALSE(true);
}
allFlags += folly::sformat("{} {} {} {}\n", name, value, reason, typeName);
});
EXPECT_EQ(
allFlags,
"follytest_internal_flag_to_a 789 default int\n"
"follytest_internal_flag_to_b test default std::string\n"
"follytest_public_flag_to_a 456 default int\n"
"follytest_public_flag_to_b basdf default std::string\n"
"follytest_some_flag default default std::string\n"
"follytest_unused unused_default default std::string\n");
EXPECT_EQ(a_ns::a_func(), 1245);
EXPECT_EQ(b_ns::b_func(), "testbasdf");
EXPECT_EQ(*some_ns::SETTING_follytest_some_flag, "default");
a_ns::SETTING_follytest_public_flag_to_a.set(100);
EXPECT_EQ(*a_ns::SETTING_follytest_public_flag_to_a, 100);
EXPECT_EQ(a_ns::getRemote(), 100);
a_ns::setRemote(200);
EXPECT_EQ(*a_ns::SETTING_follytest_public_flag_to_a, 200);
EXPECT_EQ(a_ns::getRemote(), 200);
auto res = folly::settings::getAsString("follytest_public_flag_to_a");
EXPECT_TRUE(res.hasValue());
EXPECT_EQ(res->first, "200");
EXPECT_EQ(res->second, "remote_set");
res = folly::settings::getAsString("follytest_nonexisting");
EXPECT_FALSE(res.hasValue());
EXPECT_TRUE(folly::settings::setFromString(
"follytest_public_flag_to_a", "300", "from_string"));
EXPECT_EQ(*a_ns::SETTING_follytest_public_flag_to_a, 300);
EXPECT_EQ(a_ns::getRemote(), 300);
res = folly::settings::getAsString("follytest_public_flag_to_a");
EXPECT_TRUE(res.hasValue());
EXPECT_EQ(res->first, "300");
EXPECT_EQ(res->second, "from_string");
EXPECT_FALSE(folly::settings::setFromString(
"follytest_nonexisting", "300", "from_string"));
EXPECT_TRUE(folly::settings::resetToDefault("follytest_public_flag_to_a"));
EXPECT_EQ(*a_ns::SETTING_follytest_public_flag_to_a, 456);
EXPECT_EQ(a_ns::getRemote(), 456);
EXPECT_FALSE(folly::settings::resetToDefault("follytest_nonexisting"));
}
/*
* Copyright 2018-present Facebook, Inc.
*
* Licensed 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 "a.h"
#include <folly/experimental/settings/Settings.h>
namespace a_ns {
namespace {
FOLLY_SETTING(follytest, int, internal_flag_to_a, 789, "Desc of int");
}
int a_func() {
return *SETTING_follytest_public_flag_to_a +
*SETTING_follytest_internal_flag_to_a;
}
void setRemote(int value) {
SETTING_follytest_public_flag_to_a.set(value, "remote_set");
}
int getRemote() {
return *SETTING_follytest_public_flag_to_a;
}
} // namespace a_ns
/*
* Copyright 2018-present Facebook, Inc.
*
* Licensed 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.
*/
#pragma once
#include <folly/experimental/settings/Settings.h>
namespace a_ns {
FOLLY_SETTING_SHARED(follytest, int, public_flag_to_a, 456, "Public flag to a");
int a_func();
void setRemote(int value);
int getRemote();
} // namespace a_ns
/*
* Copyright 2018-present Facebook, Inc.
*
* Licensed 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 "b.h"
#include <folly/experimental/settings/Settings.h>
namespace b_ns {
namespace {
FOLLY_SETTING(
follytest,
std::string,
internal_flag_to_b,
"test",
"Desc of str");
}
std::string b_func() {
return *SETTING_follytest_internal_flag_to_b +
*SETTING_follytest_public_flag_to_b;
}
} // namespace b_ns
/*
* Copyright 2018-present Facebook, Inc.
*
* Licensed 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.
*/
#pragma once
#include <folly/experimental/settings/Settings.h>
namespace b_ns {
FOLLY_SETTING_SHARED(
follytest,
std::string,
public_flag_to_b,
"basdf",
"Public flag to b");
std::string b_func();
} // namespace b_ns
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment