From 2fcb96be06a8c657e2b17760fbe55d8800a98e01 Mon Sep 17 00:00:00 2001 From: Chip Turner Date: Thu, 22 May 2014 20:17:14 -0700 Subject: [PATCH] Try again: folly::Singleton, a class for managing singletons Summary: Singletons are surprisingly tricky in a codebase where libraries depend on one another. folly::Singleton hopes to make this process more reliable by ensuring object creation happens in a safe order, that destruction is possible, and that singletons are created on-demand. The basic fbcode use intention is to invoke registration completion in initFacebook, so users need only declare singletons via Singleton in their .cpp files. This diff ties the Singletons into the core Init process, but not hhvm (which will be a separate diff). Test Plan: runtests Reviewed By: joelm@fb.com, hans@fb.com Subscribers: fbcode-common-diffs@, hphp-diffs@, soren, anca, lins, aalexandre, ps, trunkagent, lucian, hannesr, yfeldblum, maxwellsayles FB internal diff: D1453135 --- folly/Exception.h | 14 + folly/experimental/Singleton.cpp | 54 ++++ folly/experimental/Singleton.h | 305 ++++++++++++++++++++++ folly/experimental/test/SingletonTest.cpp | 291 +++++++++++++++++++++ 4 files changed, 664 insertions(+) create mode 100644 folly/experimental/Singleton.cpp create mode 100644 folly/experimental/Singleton.h create mode 100644 folly/experimental/test/SingletonTest.cpp diff --git a/folly/Exception.h b/folly/Exception.h index f0547c02..b4f6da14 100644 --- a/folly/Exception.h +++ b/folly/Exception.h @@ -109,6 +109,20 @@ void checkFopenErrorExplicit(FILE* fp, int savedErrno, Args&&... args) { } } +template +void throwOnFail(V&& value, Args&&... args) { + if (!value) { + throw E(std::forward(args)...); + } +} + +/** + * If cond is not true, raise an exception of type E. E must have a ctor that + * works with const char* (a description of the failure). + */ +#define CHECK_THROW(cond, E) \ + ::folly::throwOnFail((cond), "Check failed: " #cond) + } // namespace folly #endif /* FOLLY_EXCEPTION_H_ */ diff --git a/folly/experimental/Singleton.cpp b/folly/experimental/Singleton.cpp new file mode 100644 index 00000000..e02b8ca4 --- /dev/null +++ b/folly/experimental/Singleton.cpp @@ -0,0 +1,54 @@ +/* + * Copyright 2014 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +namespace folly { + +SingletonVault::~SingletonVault() { destroyInstances(); } + +void SingletonVault::destroyInstances() { + std::lock_guard guard(mutex_); + CHECK_GE(singletons_.size(), creation_order_.size()); + + for (auto type_iter = creation_order_.rbegin(); + type_iter != creation_order_.rend(); + ++type_iter) { + auto type = *type_iter; + auto it = singletons_.find(type); + CHECK(it != singletons_.end()); + auto& entry = it->second; + std::lock_guard entry_guard(entry->mutex_); + if (entry->instance.use_count() > 1) { + LOG(ERROR) << "Singleton of type " << type.name() << " has a living " + << "reference at destroyInstances time; beware! Raw pointer " + << "is " << entry->instance.get() << " with use_count of " + << entry->instance.use_count(); + } + entry->instance.reset(); + entry->state = SingletonEntryState::Dead; + } + + creation_order_.clear(); +} + +SingletonVault* SingletonVault::singleton() { + static SingletonVault vault; + return &vault; +} +} diff --git a/folly/experimental/Singleton.h b/folly/experimental/Singleton.h new file mode 100644 index 00000000..aa3130fe --- /dev/null +++ b/folly/experimental/Singleton.h @@ -0,0 +1,305 @@ +/* + * Copyright 2014 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// SingletonVault - a library to manage the creation and destruction +// of interdependent singletons. +// +// Basic usage of this class is very simple; suppose you have a class +// called MyExpensiveService, and you only want to construct one (ie, +// it's a singleton), but you only want to construct it if it is used. +// +// In your .h file: +// class MyExpensiveService { ... }; +// +// In your .cpp file: +// namespace { folly::Singleton the_singleton; } +// +// Code can access it via: +// +// MyExpensiveService* instance = Singleton::get(); +// or +// std::weak_ptr instance = +// Singleton::get_weak(); +// +// The singleton will be created on demand. If the constructor for +// MyExpensiveService actually makes use of *another* Singleton, then +// the right thing will happen -- that other singleton will complete +// construction before get() returns. However, in the event of a +// circular dependency, a runtime error will occur. +// +// By default, the singleton instance is constructed via new and +// deleted via delete, but this is configurable: +// +// namespace { folly::Singleton the_singleton(create, +// destroy); } +// +// Where create and destroy are functions, Singleton::CreateFunc +// Singleton::TeardownFunc. +// +// 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 +// SingletonVault::singleton()->destroyInstances(), which invokes the +// TeardownFunc for each singleton, in the reverse order they were +// created. It is your responsibility to ensure your singletons can +// handle cases where the singletons they depend on go away, however. + +#pragma once +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace folly { + +// For actual usage, please see the Singleton class at the bottom +// of this file; that is what you will actually interact with. + +// SingletonVault is the class that manages singleton instances. It +// is unaware of the underlying types of singletons, and simply +// manages lifecycles and invokes CreateFunc and TeardownFunc when +// appropriate. In general, you won't need to interact with the +// SingletonVault itself. +// +// A vault goes through a few stages of life: +// +// 1. Registration phase; singletons can be registered, but no +// singleton can be created. +// 2. registrationComplete() has been called; singletons can no +// longer be registered, but they can be created. +// 3. A vault can return to stage 1 when destroyInstances is called. +// +// In general, you don't need to worry about any of the above; just +// ensure registrationComplete() is called near the top of your main() +// function, otherwise no singletons can be instantiated. +class SingletonVault { + public: + SingletonVault() {}; + ~SingletonVault(); + + typedef std::function TeardownFunc; + typedef std::function CreateFunc; + + // Register a singleton of a given type with the create and teardown + // functions. + void registerSingleton(const std::type_info& type, + CreateFunc create, + TeardownFunc teardown) { + std::lock_guard guard(mutex_); + + CHECK_THROW(state_ == SingletonVaultState::Registering, std::logic_error); + CHECK_THROW(singletons_.find(type) == singletons_.end(), std::logic_error); + auto& entry = singletons_[type]; + if (!entry) { + entry.reset(new SingletonEntry); + } + + std::lock_guard entry_guard(entry->mutex_); + CHECK(entry->instance == nullptr); + CHECK(create); + CHECK(teardown); + entry->create = create; + entry->teardown = teardown; + entry->state = SingletonEntryState::Dead; + } + + // Mark registration is complete; no more singletons can be + // registered at this point. + void registrationComplete() { + std::lock_guard guard(mutex_); + CHECK_THROW(state_ == SingletonVaultState::Registering, std::logic_error); + state_ = SingletonVaultState::Running; + } + + // Destroy all singletons; when complete, the vault can create + // singletons once again, or remain dormant. + void destroyInstances(); + + // Retrieve a singleton from the vault, creating it if necessary. + std::shared_ptr get_shared(const std::type_info& type) { + std::unique_lock lock(mutex_); + CHECK_THROW(state_ == SingletonVaultState::Running, std::logic_error); + + auto it = singletons_.find(type); + if (it == singletons_.end()) { + throw std::out_of_range(std::string("non-existent singleton: ") + + type.name()); + } + + auto& entry = it->second; + std::unique_lock entry_lock(entry->mutex_); + + if (entry->state == SingletonEntryState::BeingBorn) { + throw std::out_of_range(std::string("circular singleton dependency: ") + + type.name()); + } + + if (entry->instance == nullptr) { + CHECK(entry->state == SingletonEntryState::Dead); + entry->state = SingletonEntryState::BeingBorn; + + entry_lock.unlock(); + lock.unlock(); + // Can't use make_shared -- no support for a custom deleter, sadly. + auto instance = std::shared_ptr(entry->create(), entry->teardown); + lock.lock(); + entry_lock.lock(); + + CHECK(entry->state == SingletonEntryState::BeingBorn); + entry->instance = instance; + entry->state = SingletonEntryState::Living; + + creation_order_.push_back(type); + } + CHECK(entry->state == SingletonEntryState::Living); + + return entry->instance; + } + + // For testing; how many registered and living singletons we have. + size_t registeredSingletonCount() const { + std::lock_guard guard(mutex_); + return singletons_.size(); + } + + size_t livingSingletonCount() const { + std::lock_guard guard(mutex_); + size_t ret = 0; + for (const auto& p : singletons_) { + if (p.second->instance) { + ++ret; + } + } + + return ret; + } + + // A well-known vault; you can actually have others, but this is the + // default. + static SingletonVault* singleton(); + + private: + // The two stages of life for a vault, as mentioned in the class comment. + enum class SingletonVaultState { + Registering, + Running, + }; + + // Each singleton in the vault can be in three states: dead + // (registered but never created), being born (running the + // CreateFunc), and living (CreateFunc returned an instance). + enum class SingletonEntryState { + Dead, + BeingBorn, + Living, + }; + + // An actual instance of a singleton, tracking the instance itself, + // its state as described above, and the create and teardown + // functions. + struct SingletonEntry { + std::mutex mutex_; + std::shared_ptr instance; + CreateFunc create = nullptr; + TeardownFunc teardown = nullptr; + SingletonEntryState state = SingletonEntryState::Dead; + + SingletonEntry() = default; + SingletonEntry(const SingletonEntry&) = delete; + SingletonEntry& operator=(const SingletonEntry&) = delete; + SingletonEntry& operator=(SingletonEntry&&) = delete; + SingletonEntry(SingletonEntry&&) = delete; + }; + + mutable std::mutex mutex_; + typedef std::unique_ptr SingletonEntryPtr; + std::unordered_map singletons_; + std::vector creation_order_; + SingletonVaultState state_ = SingletonVaultState::Registering; +}; + +// This is the wrapper class that most users actually interact with. +// It allows for simple access to registering and instantiating +// singletons. Create instances of this class in the global scope of +// type Singleton to register your singleton for later access via +// Singleton::get(). +template +class Singleton { + public: + typedef std::function CreateFunc; + typedef std::function TeardownFunc; + + // Generally your program life cycle should be fine with calling + // get() repeatedly rather than saving the reference, and then not + // call get() during process shutdown. + static T* get(SingletonVault* vault = nullptr /* for testing */) { + return get_shared(vault).get(); + } + + // If, however, you do need to hold a reference to the specific + // singleton, you can try to do so with a weak_ptr. Avoid this when + // possible but the inability to lock the weak pointer can be a + // signal that the vault has been destroyed. + static std::weak_ptr get_weak(SingletonVault* vault = + nullptr /* for testing */) { + return std::weak_ptr(get_shared(vault)); + } + + Singleton(Singleton::CreateFunc c = nullptr, + Singleton::TeardownFunc t = nullptr, + SingletonVault* vault = nullptr /* for testing */) { + if (c == nullptr) { + c = []() { return new T; }; + } + SingletonVault::TeardownFunc teardown; + if (t == nullptr) { + teardown = [](void* v) { delete static_cast(v); }; + } else { + teardown = [t](void* v) { t(static_cast(v)); }; + } + + if (vault == nullptr) { + vault = SingletonVault::singleton(); + } + + vault->registerSingleton(typeid(T), c, teardown); + } + + private: + // Don't use this function, it's private for a reason! Using it + // would defeat the *entire purpose* of the vault in that we lose + // the ability to guarantee that, after a destroyInstances is + // called, all instances are, in fact, destroyed. You should use + // weak_ptr if you need to hold a reference to the singleton and + // guarantee briefly that it exists. + // + // Yes, you can just get the weak pointer and lock it, but hopefully + // if you have taken the time to read this far, you see why that + // would be bad. + static std::shared_ptr get_shared(SingletonVault* vault = + nullptr /* for testing */) { + return std::static_pointer_cast( + (vault ?: SingletonVault::singleton())->get_shared(typeid(T))); + } +}; +} diff --git a/folly/experimental/test/SingletonTest.cpp b/folly/experimental/test/SingletonTest.cpp new file mode 100644 index 00000000..03b94916 --- /dev/null +++ b/folly/experimental/test/SingletonTest.cpp @@ -0,0 +1,291 @@ +/* + * Copyright 2014 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include +#include + +using namespace folly; + +// A simple class that tracks how often instances of the class and +// subclasses are created, and the ordering. +struct Watchdog { + static std::vector creation_order; + Watchdog() { creation_order.push_back(this); } + + ~Watchdog() { + if (creation_order.back() != this) { + throw std::out_of_range("Watchdog destruction order mismatch"); + } + creation_order.pop_back(); + } + + Watchdog(const Watchdog&) = delete; + Watchdog& operator=(const Watchdog&) = delete; + Watchdog(Watchdog&&) noexcept = default; +}; + +std::vector Watchdog::creation_order; + +// Some basic types we use for tracking. +struct ChildWatchdog : public Watchdog {}; +struct GlobalWatchdog : public Watchdog {}; +struct UnregisteredWatchdog : public Watchdog {}; + +namespace { +Singleton global_watchdog; +} + +// Test basic global usage (the default way singletons will generally +// be used). +TEST(Singleton, BasicGlobalUsage) { + EXPECT_EQ(Watchdog::creation_order.size(), 0); + EXPECT_EQ(SingletonVault::singleton()->registeredSingletonCount(), 1); + EXPECT_EQ(SingletonVault::singleton()->livingSingletonCount(), 0); + auto wd1 = Singleton::get(); + EXPECT_NE(wd1, nullptr); + EXPECT_EQ(Watchdog::creation_order.size(), 1); + auto wd2 = Singleton::get(); + EXPECT_NE(wd2, nullptr); + EXPECT_EQ(wd1, wd2); + EXPECT_EQ(Watchdog::creation_order.size(), 1); + SingletonVault::singleton()->destroyInstances(); + EXPECT_EQ(Watchdog::creation_order.size(), 0); +} + +TEST(Singleton, MissingSingleton) { + EXPECT_THROW([]() { auto u = Singleton::get(); }(), + std::out_of_range); +} + +// Exercise some basic codepaths ensuring registration order and +// destruction order happen as expected, that instances are created +// when expected, etc etc. +TEST(Singleton, BasicUsage) { + SingletonVault vault; + + EXPECT_EQ(vault.registeredSingletonCount(), 0); + Singleton watchdog_singleton(nullptr, nullptr, &vault); + EXPECT_EQ(vault.registeredSingletonCount(), 1); + + Singleton child_watchdog_singleton(nullptr, nullptr, &vault); + EXPECT_EQ(vault.registeredSingletonCount(), 2); + + vault.registrationComplete(); + + Watchdog* s1 = Singleton::get(&vault); + EXPECT_NE(s1, nullptr); + + Watchdog* s2 = Singleton::get(&vault); + EXPECT_NE(s2, nullptr); + + EXPECT_EQ(s1, s2); + + auto s3 = Singleton::get(&vault); + EXPECT_NE(s3, nullptr); + EXPECT_NE(s2, s3); + + EXPECT_EQ(vault.registeredSingletonCount(), 2); + EXPECT_EQ(vault.livingSingletonCount(), 2); + + vault.destroyInstances(); + EXPECT_EQ(vault.registeredSingletonCount(), 2); + EXPECT_EQ(vault.livingSingletonCount(), 0); +} + +// Some pathological cases such as getting unregistered singletons, +// double registration, etc. +TEST(Singleton, NaughtyUsage) { + SingletonVault vault; + vault.registrationComplete(); + + // Unregistered. + EXPECT_THROW(Singleton::get(), std::out_of_range); + EXPECT_THROW(Singleton::get(&vault), std::out_of_range); + + // Registring singletons after registrationComplete called. + EXPECT_THROW([&vault]() { + Singleton watchdog_singleton( + nullptr, nullptr, &vault); + }(), + std::logic_error); + + EXPECT_THROW([]() { Singleton watchdog_singleton; }(), + std::logic_error); + + SingletonVault vault_2; + EXPECT_THROW(Singleton::get(&vault_2), std::logic_error); + Singleton watchdog_singleton(nullptr, nullptr, &vault_2); + // double registration + EXPECT_THROW([&vault_2]() { + Singleton watchdog_singleton( + nullptr, nullptr, &vault_2); + }(), + std::logic_error); + vault_2.destroyInstances(); + // double registration after destroy + EXPECT_THROW([&vault_2]() { + Singleton watchdog_singleton( + nullptr, nullptr, &vault_2); + }(), + std::logic_error); +} + +TEST(Singleton, SharedPtrUsage) { + SingletonVault vault; + + EXPECT_EQ(vault.registeredSingletonCount(), 0); + Singleton watchdog_singleton(nullptr, nullptr, &vault); + EXPECT_EQ(vault.registeredSingletonCount(), 1); + + Singleton child_watchdog_singleton(nullptr, nullptr, &vault); + EXPECT_EQ(vault.registeredSingletonCount(), 2); + + vault.registrationComplete(); + + Watchdog* s1 = Singleton::get(&vault); + EXPECT_NE(s1, nullptr); + + Watchdog* s2 = Singleton::get(&vault); + EXPECT_NE(s2, nullptr); + + EXPECT_EQ(s1, s2); + + auto weak_s1 = Singleton::get_weak(&vault); + auto shared_s1 = weak_s1.lock(); + EXPECT_EQ(shared_s1.get(), s1); + EXPECT_EQ(shared_s1.use_count(), 2); + + LOG(ERROR) << "The following log message regarding ref counts is expected"; + vault.destroyInstances(); + EXPECT_EQ(vault.registeredSingletonCount(), 2); + EXPECT_EQ(vault.livingSingletonCount(), 0); + + EXPECT_EQ(shared_s1.use_count(), 1); + EXPECT_EQ(shared_s1.get(), s1); + + auto locked_s1 = weak_s1.lock(); + EXPECT_EQ(locked_s1.get(), s1); + EXPECT_EQ(shared_s1.use_count(), 2); + locked_s1.reset(); + EXPECT_EQ(shared_s1.use_count(), 1); + shared_s1.reset(); + locked_s1 = weak_s1.lock(); + EXPECT_TRUE(weak_s1.expired()); + + Watchdog* new_s1 = Singleton::get(&vault); + EXPECT_NE(new_s1, s1); +} + +// Some classes to test singleton dependencies. NeedySingleton has a +// dependency on NeededSingleton, which happens during its +// construction. +SingletonVault needy_vault; + +struct NeededSingleton {}; +struct NeedySingleton { + NeedySingleton() { + auto unused = Singleton::get(&needy_vault); + EXPECT_NE(unused, nullptr); + } +}; + +// Ensure circular dependencies fail -- a singleton that needs itself, whoops. +SingletonVault self_needy_vault; +struct SelfNeedySingleton { + SelfNeedySingleton() { + auto unused = Singleton::get(&self_needy_vault); + EXPECT_NE(unused, nullptr); + } +}; + +TEST(Singleton, SingletonDependencies) { + Singleton needed_singleton(nullptr, nullptr, &needy_vault); + Singleton needy_singleton(nullptr, nullptr, &needy_vault); + needy_vault.registrationComplete(); + + EXPECT_EQ(needy_vault.registeredSingletonCount(), 2); + EXPECT_EQ(needy_vault.livingSingletonCount(), 0); + + auto needy = Singleton::get(&needy_vault); + EXPECT_EQ(needy_vault.livingSingletonCount(), 2); + + Singleton self_needy_singleton( + nullptr, nullptr, &self_needy_vault); + self_needy_vault.registrationComplete(); + EXPECT_THROW([]() { + Singleton::get(&self_needy_vault); + }(), + std::out_of_range); +} + +// Benchmarking a normal singleton vs a Meyers singleton vs a Folly +// singleton. Meyers are insanely fast, but (hopefully) Folly +// singletons are fast "enough." +int* getMeyersSingleton() { + static auto ret = new int(0); + return ret; +} + +int normal_singleton_value = 0; +int* getNormalSingleton() { + doNotOptimizeAway(&normal_singleton_value); + return &normal_singleton_value; +} + +struct BenchmarkSingleton { + int val = 0; +}; + +BENCHMARK(NormalSingleton, n) { + for (int i = 0; i < n; ++i) { + doNotOptimizeAway(getNormalSingleton()); + } +} + +BENCHMARK_RELATIVE(MeyersSingleton, n) { + for (int i = 0; i < n; ++i) { + doNotOptimizeAway(getMeyersSingleton()); + } +} + +BENCHMARK_RELATIVE(FollySingleton, n) { + SingletonVault benchmark_vault; + Singleton benchmark_singleton( + nullptr, nullptr, &benchmark_vault); + benchmark_vault.registrationComplete(); + + for (int i = 0; i < n; ++i) { + doNotOptimizeAway(Singleton::get(&benchmark_vault)); + } +} + +int main(int argc, char* argv[]) { + testing::InitGoogleTest(&argc, argv); + google::InitGoogleLogging(argv[0]); + google::ParseCommandLineFlags(&argc, &argv, true); + + SingletonVault::singleton()->registrationComplete(); + + auto ret = RUN_ALL_TESTS(); + if (!ret) { + folly::runBenchmarksOnFlag(); + } + return ret; +} -- 2.34.1