Skip to content

Commit

Permalink
folly Singleton: "eager" option to initialize upfront
Browse files Browse the repository at this point in the history
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 facebook#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
  • Loading branch information
Steve O'Brien authored and korovkin committed Sep 30, 2015
1 parent 3a882fb commit a625f22
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 26 deletions.
35 changes: 25 additions & 10 deletions folly/Singleton-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,39 +135,54 @@ SingletonHolder<T>::SingletonHolder(TypeDescriptor type__,
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>
void SingletonHolder<T>::createInstance() {
// There's no synchronization here, so we may not see the current value
// for creating_thread if it was set by other thread, but we only care about
// it if it was set by current thread anyways.
if (creating_thread_ == std::this_thread::get_id()) {
if (creating_thread_.load(std::memory_order_acquire) ==
std::this_thread::get_id()) {
LOG(FATAL) << "circular singleton dependency: " << type_.name();
}

std::lock_guard<std::mutex> entry_lock(mutex_);
if (state_ == SingletonHolderState::Living) {
if (state_.load(std::memory_order_acquire) == SingletonHolderState::Living) {
return;
}
if (state_ == SingletonHolderState::NotRegistered) {
if (state_.load(std::memory_order_acquire) ==
SingletonHolderState::NotRegistered) {
auto ptr = SingletonVault::stackTraceGetter().load();
LOG(FATAL) << "Creating instance for unregistered singleton: "
<< type_.name() << "\n"
<< "Stacktrace:"
<< "\n" << (ptr ? (*ptr)() : "(not available)");
}

if (state_ == SingletonHolderState::Living) {
if (state_.load(std::memory_order_acquire) == SingletonHolderState::Living) {
return;
}

SCOPE_EXIT {
// 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
// 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_);
if (vault_.state_ == SingletonVault::SingletonVaultState::Quiescing) {
Expand Down Expand Up @@ -216,7 +231,7 @@ void SingletonHolder<T>::createInstance() {

// This has to be the last step, because once state is Living other threads
// 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_);
Expand Down
155 changes: 139 additions & 16 deletions folly/Singleton.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@
// Where create and destroy are functions, Singleton<T>::CreateFunc
// 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
// your singletons manage threads, but you need to fork? Or your unit
// test wants to clean up all global state? Then you can call
Expand All @@ -89,6 +109,7 @@
#include <folly/Memory.h>
#include <folly/RWSpinLock.h>
#include <folly/Demangle.h>
#include <folly/Executor.h>
#include <folly/io/async/Request.h>

#include <algorithm>
Expand All @@ -98,6 +119,7 @@
#include <condition_variable>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include <typeinfo>
#include <typeindex>
Expand Down Expand Up @@ -197,6 +219,8 @@ class SingletonHolderBase {

virtual TypeDescriptor type() = 0;
virtual bool hasLiveInstance() = 0;
virtual void createInstance() = 0;
virtual bool creationStarted() = 0;
virtual void destroyInstance() = 0;

protected:
Expand Down Expand Up @@ -224,15 +248,15 @@ struct SingletonHolder : public SingletonHolderBase {

void registerSingleton(CreateFunc c, TeardownFunc t);
void registerSingletonMock(CreateFunc c, TeardownFunc t);
virtual TypeDescriptor type();
virtual bool hasLiveInstance();
virtual void destroyInstance();
virtual TypeDescriptor type() override;
virtual bool hasLiveInstance() override;
virtual void createInstance() override;
virtual bool creationStarted() override;
virtual void destroyInstance() override;

private:
SingletonHolder(TypeDescriptor type, SingletonVault& vault);

void createInstance();

enum class SingletonHolderState {
NotRegistered,
Dead,
Expand All @@ -250,7 +274,7 @@ struct SingletonHolder : public SingletonHolderBase {
std::atomic<SingletonHolderState> state_{SingletonHolderState::NotRegistered};

// 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.

Expand Down Expand Up @@ -312,26 +336,99 @@ class SingletonVault {
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
// registered at this point.
void registrationComplete() {
// registered at this point. Kicks off eagerly-initialized singletons
// (if requested; default behavior is to do so).
void registrationComplete(bool autoStartEagerInit = true) {
RequestContext::saveContext();
std::atexit([](){ SingletonVault::singleton()->destroyInstances(); });

RWSpinLock::WriteHolder wh(&stateMutex_);
{
RWSpinLock::WriteHolder wh(&stateMutex_);

stateCheck(SingletonVaultState::Running);
stateCheck(SingletonVaultState::Running);

if (type_ == Type::Strict) {
for (const auto& p: singletons_) {
if (p.second->hasLiveInstance()) {
throw std::runtime_error(
"Singleton created before registration was complete.");
if (type_ == Type::Strict) {
for (const auto& p: singletons_) {
if (p.second->hasLiveInstance()) {
throw std::runtime_error(
"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
Expand Down Expand Up @@ -421,6 +518,8 @@ class SingletonVault {

mutable folly::RWSpinLock mutex_;
SingletonMap singletons_;
std::unordered_set<detail::SingletonHolderBase*> eagerInitSingletons_;
folly::Executor* eagerInitExecutor_{nullptr};
std::vector<detail::TypeDescriptor> creation_order_;
SingletonVaultState state_{SingletonVaultState::Running};
bool registrationComplete_{false};
Expand Down Expand Up @@ -489,6 +588,30 @@ class Singleton {
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.
* Unlike regular singletons which are initialized once per process lifetime,
Expand Down
Loading

0 comments on commit a625f22

Please sign in to comment.