Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions openfeature/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ cc_library(
],
)

cc_library(
name = "global_hook_manager",
srcs = ["global_hook_manager.cpp"],
hdrs = ["global_hook_manager.h"],
include_prefix = "openfeature",
deps = [
":general_hook",
],
)

cc_library(
name = "hook_context",
srcs = ["hook_context.cpp"],
Expand Down Expand Up @@ -227,6 +237,7 @@ cc_library(
":client_api",
":evaluation_context",
":global_context_manager",
":global_hook_manager",
":metadata",
":openfeature",
":provider",
Expand Down
45 changes: 45 additions & 0 deletions openfeature/global_hook_manager.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include "openfeature/global_hook_manager.h"

#include <memory>
#include <mutex>
#include <shared_mutex>
#include <utility>
#include <vector>

#include "openfeature/general_hook.h"

namespace openfeature {

GlobalHookManager& GlobalHookManager::GetInstance() {
static GlobalHookManager instance;
return instance;
}

void GlobalHookManager::AddHooks(
std::vector<std::shared_ptr<GeneralHook>> hooks) {
std::unique_lock<std::shared_mutex> lock(hooks_mutex_);
hooks_.reserve(hooks_.size() + hooks.size());
for (auto& hook : hooks) {
if (hook != nullptr) {
hooks_.push_back(std::move(hook));
}
}
}

void GlobalHookManager::AddHook(std::shared_ptr<GeneralHook> hook) {
if (hook == nullptr) return;
std::unique_lock<std::shared_mutex> lock(hooks_mutex_);
hooks_.push_back(std::move(hook));
}

std::vector<std::shared_ptr<GeneralHook>> GlobalHookManager::GetHooks() const {
std::shared_lock<std::shared_mutex> lock(hooks_mutex_);
return hooks_;
}

void GlobalHookManager::ClearHooks() {
std::unique_lock<std::shared_mutex> lock(hooks_mutex_);
hooks_.clear();
}

} // namespace openfeature
42 changes: 42 additions & 0 deletions openfeature/global_hook_manager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#ifndef CPP_SDK_INCLUDE_OPENFEATURE_GLOBAL_HOOK_MANAGER_H_
#define CPP_SDK_INCLUDE_OPENFEATURE_GLOBAL_HOOK_MANAGER_H_

#include <memory>
#include <shared_mutex>
#include <vector>

#include "openfeature/general_hook.h"

namespace openfeature {

// Manages global hooks for the OpenFeature SDK.
// Provides thread-safe hook storage and access across the application.
class GlobalHookManager {
public:
static GlobalHookManager& GetInstance();

GlobalHookManager(const GlobalHookManager&) = delete;
GlobalHookManager& operator=(const GlobalHookManager&) = delete;

// Adds one or more global hooks, appending them to existing hooks.
// Filters out nullptr entries.
void AddHooks(std::vector<std::shared_ptr<GeneralHook>> hooks);

// Adds a single global hook. Filters out nullptr entries.
void AddHook(std::shared_ptr<GeneralHook> hook);

// Retrieves all configured global hooks.
std::vector<std::shared_ptr<GeneralHook>> GetHooks() const;

// Clears all configured global hooks.
void ClearHooks();

private:
GlobalHookManager() = default;
mutable std::shared_mutex hooks_mutex_;
std::vector<std::shared_ptr<GeneralHook>> hooks_;
};

} // namespace openfeature

#endif // CPP_SDK_INCLUDE_OPENFEATURE_GLOBAL_HOOK_MANAGER_H_
19 changes: 5 additions & 14 deletions openfeature/openfeature_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "openfeature/client_api.h"
#include "openfeature/global_context_manager.h"
#include "openfeature/global_hook_manager.h"

namespace openfeature {

Expand Down Expand Up @@ -86,30 +87,20 @@ ProviderStatus OpenFeatureAPI::GetProviderStatus(
}

void OpenFeatureAPI::AddHooks(std::vector<std::shared_ptr<GeneralHook>> hooks) {
std::unique_lock lock(hooks_mutex_);
hooks_.reserve(hooks_.size() + hooks.size());
for (auto& hook : hooks) {
if (hook != nullptr) {
hooks_.push_back(std::move(hook));
}
}
GlobalHookManager::GetInstance().AddHooks(std::move(hooks));
}

void OpenFeatureAPI::AddHook(std::shared_ptr<GeneralHook> hook) {
if (hook == nullptr) return;
std::unique_lock lock(hooks_mutex_);
hooks_.push_back(std::move(hook));
GlobalHookManager::GetInstance().AddHook(std::move(hook));
}

std::vector<std::shared_ptr<GeneralHook>> OpenFeatureAPI::GetHooks() const {
std::shared_lock lock(hooks_mutex_);
return hooks_;
return GlobalHookManager::GetInstance().GetHooks();
}

void OpenFeatureAPI::Shutdown() {
provider_repository_.Shutdown();
std::unique_lock lock(hooks_mutex_);
hooks_.clear();
GlobalHookManager::GetInstance().ClearHooks();
}

} // namespace openfeature
3 changes: 1 addition & 2 deletions openfeature/openfeature_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "openfeature/evaluation_context.h"
#include "openfeature/general_hook.h"
#include "openfeature/global_context_manager.h"
#include "openfeature/global_hook_manager.h"
#include "openfeature/metadata.h"
#include "openfeature/openfeature.h"
#include "openfeature/provider.h"
Expand Down Expand Up @@ -85,8 +86,6 @@ class OpenFeatureAPI : public OpenFeature {

private:
ProviderRepository provider_repository_;
mutable std::shared_mutex hooks_mutex_;
std::vector<std::shared_ptr<GeneralHook>> hooks_;

OpenFeatureAPI();
};
Expand Down
11 changes: 11 additions & 0 deletions test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ cc_test(
],
)

cc_test(
name = "global_hook_manager_test",
srcs = ["global_hook_manager_test.cpp"],
deps = [
"//openfeature:general_hook",
"//openfeature:global_hook_manager",
"//openfeature:hook",
"@googletest//:gtest_main",
],
)

cc_test(
name = "value_test",
srcs = ["value_test.cpp"],
Expand Down
138 changes: 138 additions & 0 deletions test/global_hook_manager_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#include "openfeature/global_hook_manager.h"

#include <gtest/gtest.h>

#include <atomic>
#include <memory>
#include <thread>
#include <vector>

#include "openfeature/general_hook.h"
#include "openfeature/hook.h"

namespace openfeature {
namespace {

class DummyTestHook1 : public BoolHook {};
class DummyTestHook2 : public StringHook {};
class DummyTestHook3 : public IntHook {};

class GlobalHookManagerTest : public ::testing::Test {
protected:
void SetUp() override { GlobalHookManager::GetInstance().ClearHooks(); }

void TearDown() override { GlobalHookManager::GetInstance().ClearHooks(); }
};

TEST_F(GlobalHookManagerTest, ReturnsSameSingletonInstance) {
GlobalHookManager& instance1 = GlobalHookManager::GetInstance();
GlobalHookManager& instance2 = GlobalHookManager::GetInstance();

EXPECT_EQ(&instance1, &instance2);
}

TEST_F(GlobalHookManagerTest, InitialStateHasEmptyHooks) {
EXPECT_TRUE(GlobalHookManager::GetInstance().GetHooks().empty());
}

TEST_F(GlobalHookManagerTest, AddHookAppendsSingleHook) {
auto& manager = GlobalHookManager::GetInstance();
std::shared_ptr<GeneralHook> hook1 = std::make_shared<DummyTestHook1>();

manager.AddHook(hook1);

auto hooks = manager.GetHooks();
ASSERT_EQ(hooks.size(), 1);
EXPECT_EQ(hooks[0], hook1);
}

TEST_F(GlobalHookManagerTest, AddHooksAppendsMultipleHooksAndPreservesOrder) {
auto& manager = GlobalHookManager::GetInstance();
std::shared_ptr<GeneralHook> hook1 = std::make_shared<DummyTestHook1>();
std::shared_ptr<GeneralHook> hook2 = std::make_shared<DummyTestHook2>();

manager.AddHooks({hook1, hook2});

auto hooks = manager.GetHooks();
ASSERT_EQ(hooks.size(), 2);
EXPECT_EQ(hooks[0], hook1);
EXPECT_EQ(hooks[1], hook2);

std::shared_ptr<GeneralHook> hook3 = std::make_shared<DummyTestHook3>();
manager.AddHook(hook3);

hooks = manager.GetHooks();
ASSERT_EQ(hooks.size(), 3);
EXPECT_EQ(hooks[0], hook1);
EXPECT_EQ(hooks[1], hook2);
EXPECT_EQ(hooks[2], hook3);
}

TEST_F(GlobalHookManagerTest, AddHookAndAddHooksFiltersNullptrs) {
auto& manager = GlobalHookManager::GetInstance();
manager.AddHook(nullptr);
EXPECT_TRUE(manager.GetHooks().empty());

std::shared_ptr<GeneralHook> valid_hook = std::make_shared<DummyTestHook1>();
manager.AddHooks({nullptr, valid_hook, nullptr});

auto hooks = manager.GetHooks();
ASSERT_EQ(hooks.size(), 1);
EXPECT_EQ(hooks[0], valid_hook);
}

TEST_F(GlobalHookManagerTest, ClearHooksRemovesAllHooks) {
auto& manager = GlobalHookManager::GetInstance();
std::shared_ptr<GeneralHook> hook1 = std::make_shared<DummyTestHook1>();
std::shared_ptr<GeneralHook> hook2 = std::make_shared<DummyTestHook2>();
manager.AddHooks({hook1, hook2});

ASSERT_EQ(manager.GetHooks().size(), 2);

manager.ClearHooks();
EXPECT_TRUE(manager.GetHooks().empty());
}

constexpr int kWriterSleepIntervalMs = 1;
constexpr int kReaderThreadCount = 8;
constexpr int kStressTestDurationMs = 100;

TEST_F(GlobalHookManagerTest, ThreadSafetyStressTest) {
auto& manager = GlobalHookManager::GetInstance();
std::atomic<bool> stop{false};

// Writer thread adding hooks
std::thread writer([&]() {
while (!stop) {
manager.AddHook(std::make_shared<DummyTestHook1>());
std::this_thread::sleep_for(
std::chrono::milliseconds(kWriterSleepIntervalMs));
}
});

// Reader threads reading hooks
std::vector<std::thread> readers;
readers.reserve(kReaderThreadCount);
for (int i = 0; i < kReaderThreadCount; ++i) {
readers.emplace_back([&]() {
while (!stop) {
auto hooks = manager.GetHooks();
volatile size_t hook_count = hooks.size();
(void)hook_count;
}
});
}

std::this_thread::sleep_for(std::chrono::milliseconds(kStressTestDurationMs));

stop = true;
writer.join();
for (auto& reader_thread : readers) {
reader_thread.join();
}

EXPECT_FALSE(manager.GetHooks().empty());
}

} // namespace
} // namespace openfeature