Commit e4527fb5 authored by Steve O'Brien's avatar Steve O'Brien Committed by facebook-github-bot-9

folly Singleton: "eager" option to initialize upfront

Summary: Instead of the default lazy-loading behavior (still the default) some singletons might need to get initialized at startup time.  This would be for singletons which take a long time for the instance's constructor to run, e.g. expensive initialization by reading some large dataset, talking to an outside service, and so on.

Provides a way for singletons to opt-in to this, and get populated at the time  `registrationComplete()` is called, instead of lazily.

Some notes about the way I implemented here, mainly, why I did this as a "builder-pattern" kind of thing and not some other way.  I could probably be convinced to do otherwise. :)

* Changing the constructor: the constructor's already slightly fiddly with the two optional -- well, one optional construct function, and another optional-but-only-if-construct-provided, destruct function.  Didn't want to pile more into the ctor.
* New superclass called `EagerLoadedSingleton`; just didn't want to add more classes, esp. if it's just to add one more option.
* Method like `void setEagerLoad()` that makes this eager-load; not sure where one would write the `shouldEagerLoad()` call, probably in some central initialization spot in `main()`, but all the maintenance would have to go there.  I like that it's "attached" to the singleton being defined.  (Though you can still do this.)  Bonus #2; the rule that builds the cpp containing "main" doesn't need to import this dependency and the cpp doesn't have to include Singleton just to do this eager-load call, nor the header for the type itself.
* Omitting this altogether and just saying `folly::Singleton<Foo>::get_weak()` to "ping" the singleton and bring into existence: see last point.  Still might need to have the file containing this initialization decorum include/link against Foo, as well as have one place to maintain the list of things to load up-front.

Reviewed By: @meyering

Differential Revision: D2449081
parent 2171293f
...@@ -135,20 +135,35 @@ SingletonHolder<T>::SingletonHolder(TypeDescriptor type__, ...@@ -135,20 +135,35 @@ SingletonHolder<T>::SingletonHolder(TypeDescriptor type__,
type_(type__), vault_(vault) { type_(type__), vault_(vault) {
} }
template <typename T>
bool SingletonHolder<T>::creationStarted() {
// If alive, then creation was of course started.
// This is flipped after creating_thread_ was set, and before it was reset.
if (state_.load(std::memory_order_acquire) == SingletonHolderState::Living) {
return true;
}
// Not yet built. Is it currently in progress?
if (creating_thread_.load(std::memory_order_acquire) != std::thread::id()) {
return true;
}
return false;
}
template <typename T> template <typename T>
void SingletonHolder<T>::createInstance() { void SingletonHolder<T>::createInstance() {
// There's no synchronization here, so we may not see the current value if (creating_thread_.load(std::memory_order_acquire) ==
// for creating_thread if it was set by other thread, but we only care about std::this_thread::get_id()) {
// it if it was set by current thread anyways.
if (creating_thread_ == std::this_thread::get_id()) {
LOG(FATAL) << "circular singleton dependency: " << type_.name(); LOG(FATAL) << "circular singleton dependency: " << type_.name();
} }
std::lock_guard<std::mutex> entry_lock(mutex_); std::lock_guard<std::mutex> entry_lock(mutex_);
if (state_ == SingletonHolderState::Living) { if (state_.load(std::memory_order_acquire) == SingletonHolderState::Living) {
return; return;
} }
if (state_ == SingletonHolderState::NotRegistered) { if (state_.load(std::memory_order_acquire) ==
SingletonHolderState::NotRegistered) {
auto ptr = SingletonVault::stackTraceGetter().load(); auto ptr = SingletonVault::stackTraceGetter().load();
LOG(FATAL) << "Creating instance for unregistered singleton: " LOG(FATAL) << "Creating instance for unregistered singleton: "
<< type_.name() << "\n" << type_.name() << "\n"
...@@ -156,7 +171,7 @@ void SingletonHolder<T>::createInstance() { ...@@ -156,7 +171,7 @@ void SingletonHolder<T>::createInstance() {
<< "\n" << (ptr ? (*ptr)() : "(not available)"); << "\n" << (ptr ? (*ptr)() : "(not available)");
} }
if (state_ == SingletonHolderState::Living) { if (state_.load(std::memory_order_acquire) == SingletonHolderState::Living) {
return; return;
} }
...@@ -164,10 +179,10 @@ void SingletonHolder<T>::createInstance() { ...@@ -164,10 +179,10 @@ void SingletonHolder<T>::createInstance() {
// Clean up creator thread when complete, and also, in case of errors here, // Clean up creator thread when complete, and also, in case of errors here,
// so that subsequent attempts don't think this is still in the process of // so that subsequent attempts don't think this is still in the process of
// being built. // being built.
creating_thread_ = std::thread::id(); creating_thread_.store(std::thread::id(), std::memory_order_release);
}; };
creating_thread_ = std::this_thread::get_id(); creating_thread_.store(std::this_thread::get_id(), std::memory_order_release);
RWSpinLock::ReadHolder rh(&vault_.stateMutex_); RWSpinLock::ReadHolder rh(&vault_.stateMutex_);
if (vault_.state_ == SingletonVault::SingletonVaultState::Quiescing) { if (vault_.state_ == SingletonVault::SingletonVaultState::Quiescing) {
...@@ -216,7 +231,7 @@ void SingletonHolder<T>::createInstance() { ...@@ -216,7 +231,7 @@ void SingletonHolder<T>::createInstance() {
// This has to be the last step, because once state is Living other threads // This has to be the last step, because once state is Living other threads
// may access instance and instance_weak w/o synchronization. // may access instance and instance_weak w/o synchronization.
state_.store(SingletonHolderState::Living); state_.store(SingletonHolderState::Living, std::memory_order_release);
{ {
RWSpinLock::WriteHolder wh(&vault_.mutex_); RWSpinLock::WriteHolder wh(&vault_.mutex_);
......
...@@ -71,6 +71,26 @@ ...@@ -71,6 +71,26 @@
// Where create and destroy are functions, Singleton<T>::CreateFunc // Where create and destroy are functions, Singleton<T>::CreateFunc
// Singleton<T>::TeardownFunc. // Singleton<T>::TeardownFunc.
// //
// The above examples detail a situation where an expensive singleton is loaded
// on-demand (thus only if needed). However if there is an expensive singleton
// that will likely be needed, and initialization takes a potentially long time,
// e.g. while initializing, parsing some files, talking to remote services,
// making uses of other singletons, and so on, the initialization of those can
// be scheduled up front, or "eagerly".
//
// In that case the singleton can be declared this way:
//
// namespace {
// auto the_singleton =
// folly::Singleton<MyExpensiveService>(/* optional create, destroy args */)
// .shouldEagerInit();
// }
//
// This way the singleton's instance is built at program initialization
// time, or more accurately, when "registrationComplete()" or
// "startEagerInit()" is called. (More about that below; see the
// section starting with "A vault goes through a few stages of life".)
//
// What if you need to destroy all of your singletons? Say, some of // What if you need to destroy all of your singletons? Say, some of
// your singletons manage threads, but you need to fork? Or your unit // your singletons manage threads, but you need to fork? Or your unit
// test wants to clean up all global state? Then you can call // test wants to clean up all global state? Then you can call
...@@ -89,6 +109,7 @@ ...@@ -89,6 +109,7 @@
#include <folly/Memory.h> #include <folly/Memory.h>
#include <folly/RWSpinLock.h> #include <folly/RWSpinLock.h>
#include <folly/Demangle.h> #include <folly/Demangle.h>
#include <folly/Executor.h>
#include <folly/io/async/Request.h> #include <folly/io/async/Request.h>
#include <algorithm> #include <algorithm>
...@@ -98,6 +119,7 @@ ...@@ -98,6 +119,7 @@
#include <condition_variable> #include <condition_variable>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <functional> #include <functional>
#include <typeinfo> #include <typeinfo>
#include <typeindex> #include <typeindex>
...@@ -197,6 +219,8 @@ class SingletonHolderBase { ...@@ -197,6 +219,8 @@ class SingletonHolderBase {
virtual TypeDescriptor type() = 0; virtual TypeDescriptor type() = 0;
virtual bool hasLiveInstance() = 0; virtual bool hasLiveInstance() = 0;
virtual void createInstance() = 0;
virtual bool creationStarted() = 0;
virtual void destroyInstance() = 0; virtual void destroyInstance() = 0;
protected: protected:
...@@ -220,15 +244,15 @@ struct SingletonHolder : public SingletonHolderBase { ...@@ -220,15 +244,15 @@ struct SingletonHolder : public SingletonHolderBase {
void registerSingleton(CreateFunc c, TeardownFunc t); void registerSingleton(CreateFunc c, TeardownFunc t);
void registerSingletonMock(CreateFunc c, TeardownFunc t); void registerSingletonMock(CreateFunc c, TeardownFunc t);
virtual TypeDescriptor type(); virtual TypeDescriptor type() override;
virtual bool hasLiveInstance(); virtual bool hasLiveInstance() override;
virtual void destroyInstance(); virtual void createInstance() override;
virtual bool creationStarted() override;
virtual void destroyInstance() override;
private: private:
SingletonHolder(TypeDescriptor type, SingletonVault& vault); SingletonHolder(TypeDescriptor type, SingletonVault& vault);
void createInstance();
enum class SingletonHolderState { enum class SingletonHolderState {
NotRegistered, NotRegistered,
Dead, Dead,
...@@ -246,7 +270,7 @@ struct SingletonHolder : public SingletonHolderBase { ...@@ -246,7 +270,7 @@ struct SingletonHolder : public SingletonHolderBase {
std::atomic<SingletonHolderState> state_{SingletonHolderState::NotRegistered}; std::atomic<SingletonHolderState> state_{SingletonHolderState::NotRegistered};
// the thread creating the singleton (only valid while creating an object) // the thread creating the singleton (only valid while creating an object)
std::thread::id creating_thread_; std::atomic<std::thread::id> creating_thread_;
// The singleton itself and related functions. // The singleton itself and related functions.
...@@ -308,26 +332,99 @@ class SingletonVault { ...@@ -308,26 +332,99 @@ class SingletonVault {
singletons_[entry->type()] = entry; singletons_[entry->type()] = entry;
} }
/**
* Called by `Singleton<T>.shouldEagerInit()` to ensure the instance
* is built when registrationComplete() is called; see that method
* for more info.
*/
void addEagerInitSingleton(detail::SingletonHolderBase* entry) {
RWSpinLock::ReadHolder rh(&stateMutex_);
stateCheck(SingletonVaultState::Running);
if (UNLIKELY(registrationComplete_)) {
throw std::logic_error(
"Registering for eager-load after registrationComplete().");
}
RWSpinLock::ReadHolder rhMutex(&mutex_);
CHECK_THROW(singletons_.find(entry->type()) != singletons_.end(),
std::logic_error);
RWSpinLock::UpgradedHolder wh(&mutex_);
eagerInitSingletons_.insert(entry);
}
// Mark registration is complete; no more singletons can be // Mark registration is complete; no more singletons can be
// registered at this point. // registered at this point. Kicks off eagerly-initialized singletons
void registrationComplete() { // (if requested; default behavior is to do so).
void registrationComplete(bool autoStartEagerInit = true) {
RequestContext::saveContext(); RequestContext::saveContext();
std::atexit([](){ SingletonVault::singleton()->destroyInstances(); }); std::atexit([](){ SingletonVault::singleton()->destroyInstances(); });
RWSpinLock::WriteHolder wh(&stateMutex_); {
RWSpinLock::WriteHolder wh(&stateMutex_);
stateCheck(SingletonVaultState::Running); stateCheck(SingletonVaultState::Running);
if (type_ == Type::Strict) { if (type_ == Type::Strict) {
for (const auto& p: singletons_) { for (const auto& p: singletons_) {
if (p.second->hasLiveInstance()) { if (p.second->hasLiveInstance()) {
throw std::runtime_error( throw std::runtime_error(
"Singleton created before registration was complete."); "Singleton created before registration was complete.");
}
} }
} }
registrationComplete_ = true;
}
if (autoStartEagerInit) {
startEagerInit();
}
}
/**
* If eagerInitExecutor_ is non-nullptr (default is nullptr) then
* schedule eager singletons' initializations through it.
* Otherwise, initializes them synchronously, in a loop.
*/
void startEagerInit() {
std::unordered_set<detail::SingletonHolderBase*> singletonSet;
{
RWSpinLock::ReadHolder rh(&stateMutex_);
stateCheck(SingletonVaultState::Running);
if (UNLIKELY(!registrationComplete_)) {
throw std::logic_error(
"registrationComplete() not yet called");
}
singletonSet = eagerInitSingletons_; // copy set of pointers
} }
registrationComplete_ = true; auto *exe = eagerInitExecutor_; // default value is nullptr
for (auto *single : singletonSet) {
if (exe) {
eagerInitExecutor_->add([single] {
if (!single->creationStarted()) {
single->createInstance();
}
});
} else {
single->createInstance();
}
}
}
/**
* Provide an executor through which startEagerInit would run tasks.
* If there are several singletons which may be independently initialized,
* and their construction takes long, they could possibly be run in parallel
* to cut down on startup time. Unusual; default (synchronous initialization
* in a loop) is probably fine for most use cases, and most apps can most
* likely avoid using this.
*/
void setEagerInitExecutor(folly::Executor *exe) {
eagerInitExecutor_ = exe;
} }
// Destroy all singletons; when complete, the vault can't create // Destroy all singletons; when complete, the vault can't create
...@@ -417,6 +514,8 @@ class SingletonVault { ...@@ -417,6 +514,8 @@ class SingletonVault {
mutable folly::RWSpinLock mutex_; mutable folly::RWSpinLock mutex_;
SingletonMap singletons_; SingletonMap singletons_;
std::unordered_set<detail::SingletonHolderBase*> eagerInitSingletons_;
folly::Executor* eagerInitExecutor_{nullptr};
std::vector<detail::TypeDescriptor> creation_order_; std::vector<detail::TypeDescriptor> creation_order_;
SingletonVaultState state_{SingletonVaultState::Running}; SingletonVaultState state_{SingletonVaultState::Running};
bool registrationComplete_{false}; bool registrationComplete_{false};
...@@ -484,6 +583,30 @@ class Singleton { ...@@ -484,6 +583,30 @@ class Singleton {
vault->registerSingleton(&getEntry()); vault->registerSingleton(&getEntry());
} }
/**
* Should be instantiated as soon as "registrationComplete()" is called.
* Singletons are usually lazy-loaded (built on-demand) but for those which
* are known to be needed, to avoid the potential lag for objects that take
* long to construct during runtime, there is an option to make sure these
* are built up-front.
*
* Use like:
* Singleton<Foo> gFooInstance = Singleton<Foo>(...).shouldEagerInit();
*
* Or alternately, define the singleton as usual, and say
* gFooInstance.shouldEagerInit()
*
* at some point prior to calling registrationComplete().
* Then registrationComplete can be called (by default it will kick off
* init of the eager singletons); alternately, you can use
* startEagerInit().
*/
Singleton& shouldEagerInit() {
auto vault = SingletonVault::singleton<VaultTag>();
vault->addEagerInitSingleton(&getEntry());
return *this;
}
/** /**
* Construct and inject a mock singleton which should be used only from tests. * Construct and inject a mock singleton which should be used only from tests.
* Unlike regular singletons which are initialized once per process lifetime, * Unlike regular singletons which are initialized once per process lifetime,
......
...@@ -17,11 +17,13 @@ ...@@ -17,11 +17,13 @@
#include <thread> #include <thread>
#include <folly/Singleton.h> #include <folly/Singleton.h>
#include <folly/io/async/EventBase.h>
#include <folly/Benchmark.h> #include <folly/Benchmark.h>
#include <glog/logging.h> #include <glog/logging.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <boost/thread/barrier.hpp>
using namespace folly; using namespace folly;
...@@ -482,6 +484,129 @@ TEST(Singleton, SingletonConcurrencyStress) { ...@@ -482,6 +484,129 @@ TEST(Singleton, SingletonConcurrencyStress) {
} }
} }
namespace {
struct EagerInitSyncTag {};
}
template <typename T, typename Tag = detail::DefaultTag>
using SingletonEagerInitSync = Singleton<T, Tag, EagerInitSyncTag>;
TEST(Singleton, SingletonEagerInitSync) {
auto& vault = *SingletonVault::singleton<EagerInitSyncTag>();
bool didEagerInit = false;
auto sing = SingletonEagerInitSync<std::string>(
[&] {didEagerInit = true; return new std::string("foo"); })
.shouldEagerInit();
vault.registrationComplete();
EXPECT_TRUE(didEagerInit);
sing.get_weak(); // (avoid compile error complaining about unused var 'sing')
}
namespace {
struct EagerInitAsyncTag {};
}
template <typename T, typename Tag = detail::DefaultTag>
using SingletonEagerInitAsync = Singleton<T, Tag, EagerInitAsyncTag>;
TEST(Singleton, SingletonEagerInitAsync) {
auto& vault = *SingletonVault::singleton<EagerInitAsyncTag>();
bool didEagerInit = false;
auto sing = SingletonEagerInitAsync<std::string>(
[&] {didEagerInit = true; return new std::string("foo"); })
.shouldEagerInit();
folly::EventBase eb;
vault.setEagerInitExecutor(&eb);
vault.registrationComplete();
EXPECT_FALSE(didEagerInit);
eb.loop();
EXPECT_TRUE(didEagerInit);
sing.get_weak(); // (avoid compile error complaining about unused var 'sing')
}
namespace {
class TestEagerInitParallelExecutor : public folly::Executor {
public:
explicit TestEagerInitParallelExecutor(const size_t threadCount) {
eventBases_.reserve(threadCount);
threads_.reserve(threadCount);
for (size_t i = 0; i < threadCount; i++) {
eventBases_.push_back(std::make_shared<folly::EventBase>());
auto eb = eventBases_.back();
threads_.emplace_back(std::make_shared<std::thread>(
[eb] { eb->loopForever(); }));
}
}
virtual ~TestEagerInitParallelExecutor() override {
for (auto eb : eventBases_) {
eb->runInEventBaseThread([eb] { eb->terminateLoopSoon(); });
}
for (auto thread : threads_) {
thread->join();
}
}
virtual void add(folly::Func func) override {
const auto index = (counter_ ++) % eventBases_.size();
eventBases_[index]->add(func);
}
private:
std::vector<std::shared_ptr<folly::EventBase>> eventBases_;
std::vector<std::shared_ptr<std::thread>> threads_;
std::atomic<size_t> counter_ {0};
};
} // namespace
namespace {
struct EagerInitParallelTag {};
}
template <typename T, typename Tag = detail::DefaultTag>
using SingletonEagerInitParallel = Singleton<T, Tag, EagerInitParallelTag>;
TEST(Singleton, SingletonEagerInitParallel) {
const static size_t kIters = 1000;
const static size_t kThreads = 20;
std::atomic<size_t> initCounter;
auto& vault = *SingletonVault::singleton<EagerInitParallelTag>();
auto sing = SingletonEagerInitParallel<std::string>(
[&] {++initCounter; return new std::string(""); })
.shouldEagerInit();
for (size_t i = 0; i < kIters; i++) {
SCOPE_EXIT {
// clean up each time
vault.destroyInstances();
vault.reenableInstances();
};
initCounter.store(0);
{
boost::barrier barrier(kThreads + 1);
TestEagerInitParallelExecutor exe(kThreads);
vault.setEagerInitExecutor(&exe);
vault.registrationComplete(false);
EXPECT_EQ(0, initCounter.load());
for (size_t j = 0; j < kThreads; j++) {
exe.add([&] {
barrier.wait();
vault.startEagerInit();
barrier.wait();
});
}
barrier.wait(); // to await all threads' readiness
barrier.wait(); // to await all threads' completion
}
EXPECT_EQ(1, initCounter.load());
sing.get_weak(); // (avoid compile error complaining about unused var)
}
}
// Benchmarking a normal singleton vs a Meyers singleton vs a Folly // Benchmarking a normal singleton vs a Meyers singleton vs a Folly
// singleton. Meyers are insanely fast, but (hopefully) Folly // singleton. Meyers are insanely fast, but (hopefully) Folly
// singletons are fast "enough." // singletons are fast "enough."
......
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