Commit 5398ad14 authored by Scott Pruett's avatar Scott Pruett Committed by Facebook GitHub Bot

allow callbacks to be set and called on updates

Summary:
Allow callbacks to be attached to a SettingCore (via SettingWrapper),
with lifetime managed by a guard object which automatically unregisters
the callback at destruction. This provides an API for clients to react
to changes in settings without polling.

Reviewed By: yfeldblum

Differential Revision: D25592427

fbshipit-source-id: a11a98ed651ef56a0fc86f5f5e4ed235f2289746
parent a7c84c39
......@@ -36,6 +36,8 @@ namespace detail {
template <class T, std::atomic<uint64_t>* TrivialPtr>
class SettingWrapper {
public:
using CallbackHandle = typename SettingCore<T>::CallbackHandle;
/**
* Returns the setting's current value.
*
......@@ -78,6 +80,19 @@ class SettingWrapper {
*/
void set(const T& t, StringPiece reason = "api") { core_.set(t, reason); }
/**
* Adds a callback to be invoked any time the setting is updated. Callback
* is not invoked for snapshot updates unless published.
*
* @param callback void function that accepts a SettingsContents with value
* and reason, to be invoked on updates
* @returns a handle object which automatically removes the callback from
* processing once destroyd
*/
CallbackHandle addCallback(typename SettingCore<T>::UpdateCallback callback) {
return core_.addCallback(std::move(callback));
}
/**
* Returns the default value this setting was constructed with.
* NOTE: SettingsMetadata is type-agnostic, so it only stores the string
......
......@@ -22,10 +22,12 @@
#include <typeindex>
#include <folly/Conv.h>
#include <folly/Function.h>
#include <folly/Range.h>
#include <folly/SharedMutex.h>
#include <folly/ThreadLocal.h>
#include <folly/Utility.h>
#include <folly/container/F14Set.h>
#include <folly/experimental/settings/SettingsMetadata.h>
#include <folly/lang/Aligned.h>
......@@ -308,6 +310,7 @@ class SettingCore : public SettingCoreBase {
return;
}
{
SharedMutex::WriteHolder lg(globalLock_);
if (globalValue_) {
......@@ -322,9 +325,44 @@ class SettingCore : public SettingCoreBase {
}
*settingVersion_ = nextGlobalVersion();
}
invokeCallbacks(Contents(reason.str(), t));
}
const T& defaultValue() const { return defaultValue_; }
using UpdateCallback = folly::Function<void(const Contents&)>;
class CallbackHandle {
public:
CallbackHandle(
std::shared_ptr<UpdateCallback> callback,
SettingCore<T>& setting)
: callback_(std::move(callback)), setting_(setting) {}
~CallbackHandle() {
if (callback_) {
SharedMutex::WriteHolder lg(setting_.globalLock_);
setting_.callbacks_.erase(callback_);
}
}
CallbackHandle(const CallbackHandle&) = delete;
CallbackHandle& operator=(const CallbackHandle&) = delete;
CallbackHandle(CallbackHandle&&) = default;
CallbackHandle& operator=(CallbackHandle&&) = default;
private:
std::shared_ptr<UpdateCallback> callback_;
SettingCore<T>& setting_;
};
CallbackHandle addCallback(UpdateCallback callback) {
auto callbackPtr = copy_to_shared_ptr(std::move(callback));
auto copiedPtr = callbackPtr;
{
SharedMutex::WriteHolder lg(globalLock_);
callbacks_.emplace(std::move(copiedPtr));
}
return CallbackHandle(std::move(callbackPtr), *this);
}
SettingCore(
SettingMetadata meta,
T defaultValue,
......@@ -350,6 +388,8 @@ class SettingCore : public SettingCoreBase {
std::atomic<uint64_t>& trivialStorage_;
folly::F14FastSet<std::shared_ptr<UpdateCallback>> callbacks_;
/* Thread local versions start at 0, this will force a read on first access.
*/
cacheline_aligned<std::atomic<Version>> settingVersion_{in_place, 1};
......@@ -375,6 +415,20 @@ class SettingCore : public SettingCoreBase {
}
return value.second;
}
void invokeCallbacks(const Contents& contents) {
auto callbacksSnapshot = invoke([&] {
SharedMutex::ReadHolder lg(globalLock_);
// invoking arbitrary user code under the lock is dangerous
return std::vector<std::shared_ptr<UpdateCallback>>(
callbacks_.begin(), callbacks_.end());
});
for (auto& callbackPtr : callbacksSnapshot) {
auto& callback = *callbackPtr;
callback(contents);
}
}
};
} // namespace detail
......
......@@ -447,3 +447,49 @@ TEST(Settings, snapshot) {
123);
}
}
TEST(SettingsTest, callback) {
size_t callbackInvocations = 0;
std::string lastCallbackValue;
EXPECT_EQ(*some_ns::FOLLY_SETTING(follytest, some_flag), "default");
{
auto handle = some_ns::FOLLY_SETTING(follytest, some_flag)
.addCallback([&](const auto& contents) {
++callbackInvocations;
lastCallbackValue = contents.value;
});
some_ns::FOLLY_SETTING(follytest, some_flag).set("a");
EXPECT_EQ(callbackInvocations, 1);
EXPECT_EQ(lastCallbackValue, "a");
size_t secondCallbackInvocations = 0;
// Test adding multiple callbacks and letting the handle go out of scope
{
auto secondHandle = some_ns::FOLLY_SETTING(follytest, some_flag)
.addCallback([&](const auto& /* contents */) {
++secondCallbackInvocations;
});
some_ns::FOLLY_SETTING(follytest, some_flag).set("b");
EXPECT_EQ(callbackInvocations, 2);
EXPECT_EQ(lastCallbackValue, "b");
EXPECT_EQ(secondCallbackInvocations, 1);
}
some_ns::FOLLY_SETTING(follytest, some_flag).set("c");
EXPECT_EQ(callbackInvocations, 3);
EXPECT_EQ(lastCallbackValue, "c");
// Second callback no longer invoked
EXPECT_EQ(secondCallbackInvocations, 1);
auto movedHandle = std::move(handle);
some_ns::FOLLY_SETTING(follytest, some_flag).set("d");
EXPECT_EQ(callbackInvocations, 4);
EXPECT_EQ(lastCallbackValue, "d");
}
// Main callback no longer invoked
some_ns::FOLLY_SETTING(follytest, some_flag).set("e");
EXPECT_EQ(callbackInvocations, 4);
EXPECT_EQ(lastCallbackValue, "d");
}
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