Commit 845a46cc authored by Anton Likhtarov's avatar Anton Likhtarov Committed by Facebook Github Bot

Basic Snapshot functionality

Summary:
See the unit test for example usage.

```
folly::settings::Snapshot snapshot;

// Now changes to `FOLLY_SETTING(project, name)` and `snapshot(FOLLY_SETTING(project, name))`
// are independent from each other.
// Any updates to the settings in the snapshot can be published via snapshot.publish()
```

Basic version with a global lock, there are ways to optimize this later.

Differential Revision: D9926070

fbshipit-source-id: 66ee7f28539c242a08b688eab4a18e4a42a8b0ff
parent 66a3d1c1
......@@ -104,5 +104,79 @@ void forEachSetting(
}
}
namespace detail {
std::atomic<SettingCoreBase::Version> gGlobalVersion_;
auto& getSavedValuesMutex() {
static SharedMutex gSavedValuesMutex;
return gSavedValuesMutex;
}
/* Version -> (count of outstanding snapshots, saved setting values) */
auto& getSavedValues() {
static std::unordered_map<
SettingCoreBase::Version,
std::pair<size_t, std::unordered_map<SettingCoreBase::Key, BoxedValue>>>
gSavedValues;
return gSavedValues;
}
SettingCoreBase::Version nextGlobalVersion() {
return gGlobalVersion_.fetch_add(1) + 1;
}
void saveValueForOutstandingSnapshots(
SettingCoreBase::Key settingKey,
SettingCoreBase::Version version,
const BoxedValue& value) {
SharedMutex::WriteHolder lg(getSavedValuesMutex());
for (auto& it : getSavedValues()) {
if (version <= it.first) {
it.second.second[settingKey] = value;
}
}
}
const BoxedValue* FOLLY_NULLABLE
getSavedValue(SettingCoreBase::Key settingKey, SettingCoreBase::Version at) {
SharedMutex::ReadHolder lg(getSavedValuesMutex());
auto it = getSavedValues().find(at);
if (it != getSavedValues().end()) {
auto jt = it->second.second.find(settingKey);
if (jt != it->second.second.end()) {
return &jt->second;
}
}
return nullptr;
}
SnapshotBase::SnapshotBase() {
SharedMutex::WriteHolder lg(detail::getSavedValuesMutex());
at_ = detail::gGlobalVersion_.load();
auto it = detail::getSavedValues().emplace(
std::piecewise_construct,
std::forward_as_tuple(at_),
std::forward_as_tuple());
++it.first->second.first;
}
SnapshotBase::~SnapshotBase() {
SharedMutex::WriteHolder lg(detail::getSavedValuesMutex());
auto it = detail::getSavedValues().find(at_);
assert(it != detail::getSavedValues().end());
--it->second.first;
if (!it->second.first) {
detail::getSavedValues().erase(at_);
}
}
} // namespace detail
void Snapshot::publish() {
for (auto& it : snapshotValues_) {
it.second.publish();
}
}
} // namespace settings
} // namespace folly
......@@ -24,6 +24,8 @@
namespace folly {
namespace settings {
class Snapshot;
namespace detail {
/**
......@@ -58,8 +60,6 @@ class SettingWrapper {
* @throws std::runtime_error If we can't convert t to string.
*/
void set(const T& t, StringPiece reason = "api") {
/* Check that we can still display it */
folly::to<std::string>(t);
core_.set(t, reason);
}
......@@ -67,6 +67,7 @@ class SettingWrapper {
private:
SettingCore<T>& core_;
friend class folly::settings::Snapshot;
};
/* C++20 has std::type_indentity */
......@@ -227,5 +228,107 @@ void forEachSetting(
void(const SettingMetadata&, folly::StringPiece, folly::StringPiece)>&
func);
namespace detail {
/**
* Like SettingWrapper, but checks against any values saved/updated in a
* snapshot.
*/
template <class T>
class SnapshotSettingWrapper {
public:
/**
* The references are only valid for the duration of the snapshot's
* lifetime or until the setting has been updated in the snapshot,
* whichever happens earlier.
*/
const T& operator*() const;
const T* operator->() const {
return &operator*();
}
/**
* Update the setting in the snapshot, the effects are not visible
* in this snapshot.
*/
void set(const T& t, StringPiece reason = "api") {
core_.set(t, reason, &snapshot_);
}
private:
Snapshot& snapshot_;
SettingCore<T>& core_;
friend class folly::settings::Snapshot;
SnapshotSettingWrapper(Snapshot& snapshot, SettingCore<T>& core)
: snapshot_(snapshot), core_(core) {}
};
} // namespace detail
/**
* Captures the current state of all setting values and allows
* updating multiple settings at once, with verification and rollback.
*
* A single snapshot cannot be used concurrently from different
* threads. Multiple concurrent snapshots are safe. Passing a single
* snapshot from one thread to another is safe as long as the user
* properly synchronizes the handoff.
*
* Example usage:
*
* folly::settings::Snapshot snapshot;
* // FOLLY_SETTING(project, name) refers to the globally visible value
* // snapshot(FOLLY_SETTING(project, name)) refers to the value saved in the
* // snapshot
* FOLLY_SETTING(project, name).set(new_value);
* assert(*FOLLY_SETTING(project, name) == new_value);
* assert(*snapshot(FOLLY_SETTING(project, name)) == old_value);
*
* snapshot(FOLLY_SETTING(project, name)).set(new_snapshot_value);
* assert(*FOLLY_SETTING(project, name) == new_value);
* assert(*snapshot(FOLLY_SETTING(project, name)) == new_snapshot_value);
*
* // At this point we can discard the snapshot and forget new_snapshot_value,
* // or choose to publish:
* snapshot.publish();
* assert(*FOLLY_SETTING(project, name) == new_snapshot_value);
*/
class Snapshot final : public detail::SnapshotBase {
public:
/**
* Wraps a global FOLLY_SETTING(a, b) and returns a snapshot-local wrapper.
*/
template <class T, std::atomic<uint64_t>* P>
detail::SnapshotSettingWrapper<T> operator()(
detail::SettingWrapper<T, P>&& setting) {
return detail::SnapshotSettingWrapper<T>(*this, setting.core_);
}
/**
* Returns a snapshot of all current setting values.
* Global settings changes will not be visible in the snapshot, and vice
* versa.
*/
Snapshot() = default;
/**
* Apply all settings updates from this snapshot to the global state
* unconditionally.
*/
void publish();
private:
template <typename T>
friend class detail::SnapshotSettingWrapper;
};
namespace detail {
template <class T>
inline const T& SnapshotSettingWrapper<T>::operator*() const {
return snapshot_.get(core_);
}
} // namespace detail
} // namespace settings
} // namespace folly
......@@ -52,15 +52,131 @@ struct SettingContents {
class SettingCoreBase {
public:
using Key = intptr_t;
using Version = uint64_t;
virtual void setFromString(StringPiece newValue, StringPiece reason) = 0;
virtual std::pair<std::string, std::string> getAsString() const = 0;
virtual void resetToDefault() = 0;
virtual const SettingMetadata& meta() const = 0;
virtual ~SettingCoreBase() {}
/**
* Hashable key uniquely identifying this setting in this process
*/
Key getKey() const {
return reinterpret_cast<Key>(this);
}
};
void registerSetting(SettingCoreBase& core);
/**
* Returns the monotonically increasing unique positive version.
*/
SettingCoreBase::Version nextGlobalVersion();
template <class T>
class SettingCore;
/**
* Type erasure for setting values
*/
class BoxedValue {
public:
BoxedValue() = default;
/**
* Stores a value that can be retrieved later
*/
template <class T>
explicit BoxedValue(const T& value) : value_(std::make_shared<T>(value)) {}
/**
* Stores a value that can be both retrieved later and optionally
* applied globally
*/
template <class T>
BoxedValue(const T& value, folly::StringPiece reason, SettingCore<T>& core)
: value_(std::make_shared<T>(value)),
publish_([value = value_, &core, r = reason.str()]() {
core.set(unboxImpl<T>(value.get()), r);
}) {}
/**
* Returns the reference to the stored value
*/
template <class T>
const T& unbox() const {
return unboxImpl<T>(value_.get());
}
/**
* Applies the stored value globally
*/
void publish() {
if (publish_) {
publish_();
}
}
private:
std::shared_ptr<void> value_;
std::function<void()> publish_;
template <class T>
static const T& unboxImpl(void* value) {
return *static_cast<const T*>(value);
}
};
/**
* If there are any outstanding snapshots that care about this
* value that's about to be updated, save it to extend its lifetime
*/
void saveValueForOutstandingSnapshots(
SettingCoreBase::Key settingKey,
SettingCoreBase::Version version,
const BoxedValue& value);
/**
* @returns a pointer to a saved value at or before the given version
*/
const BoxedValue* getSavedValue(
SettingCoreBase::Key key,
SettingCoreBase::Version at);
class SnapshotBase {
protected:
detail::SettingCoreBase::Version at_;
std::unordered_map<detail::SettingCoreBase::Key, detail::BoxedValue>
snapshotValues_;
template <typename T>
friend class SettingCore;
SnapshotBase();
virtual ~SnapshotBase();
template <class T>
const T& get(detail::SettingCore<T>& core) const {
auto it = snapshotValues_.find(core.getKey());
if (it != snapshotValues_.end()) {
return it->second.template unbox<T>();
}
auto savedValue = detail::getSavedValue(core.getKey(), at_);
if (savedValue) {
return savedValue->template unbox<T>();
}
return core.getSlow();
}
template <class T>
void set(detail::SettingCore<T>& core, const T& t, StringPiece reason) {
snapshotValues_[core.getKey()] = detail::BoxedValue(t, reason, core);
}
};
template <class T>
std::enable_if_t<std::is_constructible<T, StringPiece>::value, T>
convertOrConstruct(StringPiece newValue) {
......@@ -123,15 +239,28 @@ class SettingCore : public SettingCoreBase {
return const_cast<SettingCore*>(this)->tlValue()->value;
}
void set(const T& t, StringPiece reason) {
void set(const T& t, StringPiece reason, SnapshotBase* snapshot = nullptr) {
/* Check that we can still display it (will throw otherwise) */
to<std::string>(t);
if (snapshot) {
snapshot->set(*this, t, reason);
return;
}
SharedMutex::WriteHolder lg(globalLock_);
if (globalValue_) {
saveValueForOutstandingSnapshots(
getKey(), *settingVersion_, BoxedValue(globalValue_->value));
}
globalValue_ = std::make_shared<Contents>(reason.str(), t);
if (IsSmallPOD<T>::value) {
uint64_t v = 0;
std::memcpy(&v, &t, sizeof(T));
trivialStorage_.store(v);
}
++(*globalVersion_);
*settingVersion_ = nextGlobalVersion();
}
SettingCore(
......@@ -143,7 +272,7 @@ class SettingCore : public SettingCoreBase {
trivialStorage_(trivialStorage),
localValue_([]() {
return new CachelinePadded<
Indestructible<std::pair<size_t, std::shared_ptr<Contents>>>>(
Indestructible<std::pair<Version, std::shared_ptr<Contents>>>>(
0, nullptr);
}) {
set(defaultValue_, "default");
......@@ -159,27 +288,28 @@ class SettingCore : public SettingCoreBase {
std::atomic<uint64_t>& trivialStorage_;
/* Local versions start at 0, this will force a read on first local access. */
CachelinePadded<std::atomic<size_t>> globalVersion_{1};
/* Thread local versions start at 0, this will force a read on first access.
*/
CachelinePadded<std::atomic<Version>> settingVersion_{1};
ThreadLocal<CachelinePadded<
Indestructible<std::pair<size_t, std::shared_ptr<Contents>>>>>
Indestructible<std::pair<Version, std::shared_ptr<Contents>>>>>
localValue_;
FOLLY_ALWAYS_INLINE std::shared_ptr<Contents>& tlValue() {
auto& value = ***localValue_;
if (LIKELY(value.first == *globalVersion_)) {
if (LIKELY(value.first == *settingVersion_)) {
return value.second;
}
return tlValueSlow();
}
FOLLY_NOINLINE std::shared_ptr<Contents>& tlValueSlow() {
auto& value = ***localValue_;
while (value.first < *globalVersion_) {
while (value.first < *settingVersion_) {
/* If this destroys the old value, do it without holding the lock */
value.second.reset();
SharedMutex::ReadHolder lg(globalLock_);
value.first = *globalVersion_;
value.first = *settingVersion_;
value.second = globalValue_;
}
return value.second;
......
......@@ -223,3 +223,108 @@ TEST(Settings, basic) {
EXPECT_FALSE(folly::settings::resetToDefault("follytest_nonexisting"));
}
TEST(Settings, snapshot) {
// Test discarding a snapshot
{
folly::settings::Snapshot snapshot;
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "default");
EXPECT_EQ(
*snapshot(some_ns::FOLLY_SETTING(follytest, some_flag)), "default");
// Set the global value, snapshot doesn't see it
some_ns::FOLLY_SETTING(follytest, some_flag).set("global_value");
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "global_value");
EXPECT_EQ(
*snapshot(some_ns::FOLLY_SETTING(follytest, some_flag)), "default");
// Set the value in the snapshot only
snapshot(some_ns::FOLLY_SETTING(follytest, some_flag))
.set("snapshot_value");
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "global_value");
EXPECT_EQ(
*snapshot(some_ns::FOLLY_SETTING(follytest, some_flag)),
"snapshot_value");
}
// Discard the snapshot
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "global_value");
// Test publishing a snapshot
{
folly::settings::Snapshot snapshot;
// Set the value in the snapshot only
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "global_value");
EXPECT_EQ(
*snapshot(some_ns::FOLLY_SETTING(follytest, some_flag)),
"global_value");
snapshot(some_ns::FOLLY_SETTING(follytest, some_flag))
.set("snapshot_value2");
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "global_value");
EXPECT_EQ(
*snapshot(some_ns::FOLLY_SETTING(follytest, some_flag)),
"snapshot_value2");
// Set the global value, snapshot doesn't see it
some_ns::FOLLY_SETTING(follytest, some_flag).set("global_value2");
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "global_value2");
EXPECT_EQ(
*snapshot(some_ns::FOLLY_SETTING(follytest, some_flag)),
"snapshot_value2");
snapshot.publish();
}
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "snapshot_value2");
// Snapshots at different points in time
{
some_ns::FOLLY_SETTING(follytest, some_flag).set("a");
a_ns::FOLLY_SETTING(follytest, public_flag_to_a).set(123);
folly::settings::Snapshot snapshot_1;
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "a");
EXPECT_EQ(*snapshot_1(some_ns::FOLLY_SETTING(follytest, some_flag)), "a");
EXPECT_EQ(*a_ns::FOLLY_SETTING(follytest, public_flag_to_a), 123);
EXPECT_EQ(
*snapshot_1(a_ns::FOLLY_SETTING(follytest, public_flag_to_a)), 123);
some_ns::FOLLY_SETTING(follytest, some_flag).set("b");
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "b");
EXPECT_EQ(*snapshot_1(some_ns::FOLLY_SETTING(follytest, some_flag)), "a");
EXPECT_EQ(*a_ns::FOLLY_SETTING(follytest, public_flag_to_a), 123);
EXPECT_EQ(
*snapshot_1(a_ns::FOLLY_SETTING(follytest, public_flag_to_a)), 123);
folly::settings::Snapshot snapshot_2;
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "b");
EXPECT_EQ(*snapshot_1(some_ns::FOLLY_SETTING(follytest, some_flag)), "a");
EXPECT_EQ(*snapshot_2(some_ns::FOLLY_SETTING(follytest, some_flag)), "b");
EXPECT_EQ(*a_ns::FOLLY_SETTING(follytest, public_flag_to_a), 123);
EXPECT_EQ(
*snapshot_1(a_ns::FOLLY_SETTING(follytest, public_flag_to_a)), 123);
EXPECT_EQ(
*snapshot_2(a_ns::FOLLY_SETTING(follytest, public_flag_to_a)), 123);
some_ns::FOLLY_SETTING(follytest, some_flag).set("c");
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "c");
EXPECT_EQ(*snapshot_1(some_ns::FOLLY_SETTING(follytest, some_flag)), "a");
EXPECT_EQ(*snapshot_2(some_ns::FOLLY_SETTING(follytest, some_flag)), "b");
EXPECT_EQ(*a_ns::FOLLY_SETTING(follytest, public_flag_to_a), 123);
EXPECT_EQ(
*snapshot_1(a_ns::FOLLY_SETTING(follytest, public_flag_to_a)), 123);
EXPECT_EQ(
*snapshot_2(a_ns::FOLLY_SETTING(follytest, public_flag_to_a)), 123);
a_ns::FOLLY_SETTING(follytest, public_flag_to_a).set(456);
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "c");
EXPECT_EQ(*snapshot_1(some_ns::FOLLY_SETTING(follytest, some_flag)), "a");
EXPECT_EQ(*snapshot_2(some_ns::FOLLY_SETTING(follytest, some_flag)), "b");
EXPECT_EQ(*a_ns::FOLLY_SETTING(follytest, public_flag_to_a), 456);
EXPECT_EQ(
*snapshot_1(a_ns::FOLLY_SETTING(follytest, public_flag_to_a)), 123);
EXPECT_EQ(
*snapshot_2(a_ns::FOLLY_SETTING(follytest, public_flag_to_a)), 123);
}
}
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