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 { ...@@ -36,6 +36,8 @@ namespace detail {
template <class T, std::atomic<uint64_t>* TrivialPtr> template <class T, std::atomic<uint64_t>* TrivialPtr>
class SettingWrapper { class SettingWrapper {
public: public:
using CallbackHandle = typename SettingCore<T>::CallbackHandle;
/** /**
* Returns the setting's current value. * Returns the setting's current value.
* *
...@@ -78,6 +80,19 @@ class SettingWrapper { ...@@ -78,6 +80,19 @@ class SettingWrapper {
*/ */
void set(const T& t, StringPiece reason = "api") { core_.set(t, reason); } 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. * Returns the default value this setting was constructed with.
* NOTE: SettingsMetadata is type-agnostic, so it only stores the string * NOTE: SettingsMetadata is type-agnostic, so it only stores the string
......
...@@ -22,10 +22,12 @@ ...@@ -22,10 +22,12 @@
#include <typeindex> #include <typeindex>
#include <folly/Conv.h> #include <folly/Conv.h>
#include <folly/Function.h>
#include <folly/Range.h> #include <folly/Range.h>
#include <folly/SharedMutex.h> #include <folly/SharedMutex.h>
#include <folly/ThreadLocal.h> #include <folly/ThreadLocal.h>
#include <folly/Utility.h> #include <folly/Utility.h>
#include <folly/container/F14Set.h>
#include <folly/experimental/settings/SettingsMetadata.h> #include <folly/experimental/settings/SettingsMetadata.h>
#include <folly/lang/Aligned.h> #include <folly/lang/Aligned.h>
...@@ -308,23 +310,59 @@ class SettingCore : public SettingCoreBase { ...@@ -308,23 +310,59 @@ class SettingCore : public SettingCoreBase {
return; return;
} }
SharedMutex::WriteHolder lg(globalLock_); {
SharedMutex::WriteHolder lg(globalLock_);
if (globalValue_) {
saveValueForOutstandingSnapshots( if (globalValue_) {
getKey(), *settingVersion_, BoxedValue(*globalValue_)); saveValueForOutstandingSnapshots(
} getKey(), *settingVersion_, BoxedValue(*globalValue_));
globalValue_ = std::make_shared<Contents>(reason.str(), t); }
if (IsSmallPOD<T>::value) { globalValue_ = std::make_shared<Contents>(reason.str(), t);
uint64_t v = 0; if (IsSmallPOD<T>::value) {
std::memcpy(&v, &t, sizeof(T)); uint64_t v = 0;
trivialStorage_.store(v); std::memcpy(&v, &t, sizeof(T));
trivialStorage_.store(v);
}
*settingVersion_ = nextGlobalVersion();
} }
*settingVersion_ = nextGlobalVersion(); invokeCallbacks(Contents(reason.str(), t));
} }
const T& defaultValue() const { return defaultValue_; } 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( SettingCore(
SettingMetadata meta, SettingMetadata meta,
T defaultValue, T defaultValue,
...@@ -350,6 +388,8 @@ class SettingCore : public SettingCoreBase { ...@@ -350,6 +388,8 @@ class SettingCore : public SettingCoreBase {
std::atomic<uint64_t>& trivialStorage_; 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. /* Thread local versions start at 0, this will force a read on first access.
*/ */
cacheline_aligned<std::atomic<Version>> settingVersion_{in_place, 1}; cacheline_aligned<std::atomic<Version>> settingVersion_{in_place, 1};
...@@ -375,6 +415,20 @@ class SettingCore : public SettingCoreBase { ...@@ -375,6 +415,20 @@ class SettingCore : public SettingCoreBase {
} }
return value.second; 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 } // namespace detail
......
...@@ -447,3 +447,49 @@ TEST(Settings, snapshot) { ...@@ -447,3 +447,49 @@ TEST(Settings, snapshot) {
123); 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