Skip to content
Draft
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
78 changes: 70 additions & 8 deletions libs/client-sdk/src/client_impl.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#include "client_impl.hpp"
#include "data_sources/fdv2/fdv2_data_source.hpp"
#include "data_sources/fdv2/mode_sources.hpp"
#include "data_sources/null_data_source.hpp"
#include "data_sources/polling_data_source.hpp"
#include "data_sources/streaming_data_source.hpp"
Expand Down Expand Up @@ -31,12 +33,58 @@
using launchdarkly::config::shared::built::DataSourceConfig;
using launchdarkly::config::shared::built::HttpProperties;

using FDv2Config = config::shared::built::FDv2Config<ClientSDK>;

static bool UsesFDv2(Config const& config) {
return std::holds_alternative<FDv2Config>(config.DataSourceConfig().method);
}

static std::shared_ptr<data_sources::IDataSource> MakeFDv2DataSource(
HttpProperties const& http_properties,
Config const& config,
Context const& context,
boost::asio::any_io_executor const& executor,
flag_manager::FlagManager& flag_manager,
data_sources::DataSourceStatusManager& status_manager,
Logger& logger) {
auto const& fdv2 = std::get<FDv2Config>(config.DataSourceConfig().method);
auto const& endpoints = config.ServiceEndpoints();

if (config.DataSourceConfig().use_report) {
LD_LOG(logger, LogLevel::kWarn)
<< "UseReport is not applicable to FDv2 and will be ignored";
}

data_sources::ModeSourceParams const params{
executor,
logger,
fdv2.polling_base_url,
fdv2.streaming_base_url,
http_properties,
endpoints,
context,
config.DataSourceConfig().with_reasons,
&flag_manager.Cache()};

auto sources =
data_sources::BuildModeSources(fdv2, fdv2.initial_mode, params);

return std::make_shared<data_sources::FDv2DataSource>(
std::move(sources.initializers), std::move(sources.synchronizers),
std::make_unique<data_sources::FallbackConditionFactory>(
executor, fdv2.fallback_timeout),
std::make_unique<data_sources::RecoveryConditionFactory>(
executor, fdv2.recovery_timeout),
executor, context, &flag_manager.Updater(), &flag_manager.Store(),
&status_manager, logger);
}

static std::shared_ptr<data_sources::IDataSource> MakeDataSource(
HttpProperties const& http_properties,
Config const& config,
Context const& context,
boost::asio::any_io_executor const& executor,
IDataSourceUpdateSink& flag_updater,
flag_manager::FlagManager& flag_manager,
data_sources::DataSourceStatusManager& status_manager,
Logger& logger) {
if (config.Offline()) {
Expand All @@ -48,18 +96,26 @@

auto data_source_properties = builder.Build();

if (config.DataSourceConfig().method.index() == 0) {
if (UsesFDv2(config)) {
return MakeFDv2DataSource(data_source_properties, config, context,
executor, flag_manager, status_manager,
logger);
}

if (std::holds_alternative<
config::shared::built::StreamingConfig<ClientSDK>>(
config.DataSourceConfig().method)) {
return std::make_shared<
launchdarkly::client_side::data_sources::StreamingDataSource>(
config.ServiceEndpoints(), config.DataSourceConfig(),
data_source_properties, executor, context, flag_updater,
data_source_properties, executor, context, flag_manager.Updater(),
status_manager, logger);
}
return std::make_shared<
launchdarkly::client_side::data_sources::PollingDataSource>(
config.ServiceEndpoints(), config.DataSourceConfig(),
data_source_properties, executor, context, flag_updater, status_manager,
logger);
data_source_properties, executor, context, flag_manager.Updater(),
status_manager, logger);
}

static Logger MakeLogger(config::shared::built::Logging const& config) {
Expand Down Expand Up @@ -101,13 +157,17 @@
MakePersistence(config_)),
data_source_factory_([this]() {
return MakeDataSource(http_properties_, config_, context_,
ioc_.get_executor(), flag_manager_.Updater(),
ioc_.get_executor(), flag_manager_,
status_manager_, logger_);
}),
data_source_(nullptr),
event_processor_(nullptr),
eval_reasons_available_(config_.DataSourceConfig().with_reasons) {
flag_manager_.LoadCache(context_);
// Under FDv2 the cache is loaded by an initializer, so that it enters the
// store through the same path as every other source.
if (!UsesFDv2(config_)) {
flag_manager_.LoadCache(context_);
}

if (auto custom_ca = http_properties_.Tls().CustomCAFile()) {
LD_LOG(logger_, LogLevel::kInfo)
Expand Down Expand Up @@ -161,7 +221,9 @@
std::future<bool> ClientImpl::IdentifyAsync(Context context) {
UpdateContextSynchronized(context);
flag_manager_.ClearSelector();
flag_manager_.LoadCache(context);
if (!UsesFDv2(config_)) {
flag_manager_.LoadCache(context);
}
event_processor_->SendAsync(events::IdentifyEventParams{
std::chrono::system_clock::now(), std::move(context)});

Expand Down Expand Up @@ -212,7 +274,7 @@
std::unordered_map<Client::FlagKey, Value> result;
for (auto& [key, descriptor] : flag_manager_.Store().GetAll()) {
if (descriptor->item) {
result.try_emplace(key, descriptor->item->Detail().Value());

Check warning on line 277 in libs/client-sdk/src/client_impl.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/client-sdk/src/client_impl.cpp:277:37 [bugprone-unchecked-optional-access]

unchecked access to optional value
}
}
return result;
Expand Down Expand Up @@ -245,8 +307,8 @@
}

template <typename T>
EvaluationDetail<T> ClientImpl::VariationInternal(

Check warning on line 310 in libs/client-sdk/src/client_impl.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/client-sdk/src/client_impl.cpp:310:33 [readability-function-cognitive-complexity]

function 'VariationInternal' has cognitive complexity of 27 (threshold 25)
FlagKey const& key,

Check warning on line 311 in libs/client-sdk/src/client_impl.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/client-sdk/src/client_impl.cpp:311:5 [bugprone-easily-swappable-parameters]

2 adjacent parameters of 'VariationInternal' of convertible types are easily swapped by mistake
Value default_value,
bool check_type,
bool detailed,
Expand Down Expand Up @@ -303,7 +365,7 @@

LD_ASSERT(desc->item);

auto const& flag = *(desc->item);

Check warning on line 368 in libs/client-sdk/src/client_impl.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/client-sdk/src/client_impl.cpp:368:25 [bugprone-unchecked-optional-access]

unchecked access to optional value
auto const& detail = flag.Detail();

// The Prerequisites vector represents the evaluated prerequisites of
Expand Down
112 changes: 112 additions & 0 deletions libs/client-sdk/tests/fdv2_client_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#include <gtest/gtest.h>

#include <launchdarkly/client_side/client.hpp>
#include <launchdarkly/context_builder.hpp>

#include <chrono>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>

using namespace launchdarkly;
using namespace launchdarkly::client_side;
using namespace std::chrono_literals;

namespace {

class TestPersistence : public IPersistence {

Check warning on line 19 in libs/client-sdk/tests/fdv2_client_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/client-sdk/tests/fdv2_client_test.cpp:19:7 [cppcoreguidelines-virtual-class-destructor]

destructor of 'TestPersistence' is public and non-virtual
public:
using StoreType =
std::map<std::string,
std::map<std::string, std::optional<std::string>>>;

explicit TestPersistence(StoreType store) : store_(std::move(store)) {}

void Set(std::string storageNamespace,
std::string key,
std::string data) noexcept override {
std::lock_guard lock{mutex_};
store_[storageNamespace][key] = data;
}

void Remove(std::string storageNamespace,
std::string key) noexcept override {
std::lock_guard lock{mutex_};
store_[storageNamespace].erase(key);
}

std::optional<std::string> Read(std::string storageNamespace,

Check warning on line 40 in libs/client-sdk/tests/fdv2_client_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/client-sdk/tests/fdv2_client_test.cpp:40:37 [bugprone-easily-swappable-parameters]

2 adjacent parameters of 'Read' of similar type ('std::string') are easily swapped by mistake
std::string key) noexcept override {
std::lock_guard lock{mutex_};
auto const ns = store_.find(storageNamespace);

Check warning on line 43 in libs/client-sdk/tests/fdv2_client_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/client-sdk/tests/fdv2_client_test.cpp:43:20 [readability-identifier-length]

variable name 'ns' is too short, expected at least 3 characters
if (ns == store_.end()) {
return std::nullopt;
}
auto const entry = ns->second.find(key);
if (entry == ns->second.end()) {
return std::nullopt;
}
return entry->second;
}

private:
// The SDK reads from its own thread while the test writes from the main
// one.
std::mutex mutex_;
StoreType store_;
};

// Offline mode makes no requests, so the whole FDv2 path can be exercised
// without a service to talk to.
Config OfflineFDv2Config(std::shared_ptr<IPersistence> persistence) {
auto builder = ConfigBuilder("the-key");
builder.DataSource().Method(
DataSourceBuilder::FDv2().InitialMode(ConnectionMode::kOffline));
builder.Events().Disable();
if (persistence) {
builder.Persistence().Custom(std::move(persistence));
} else {
builder.Persistence().None();
}
return builder.Build().value();
}

// The namespace and context id the client derives for SDK key "the-key" and
// context user:user-key.
char const* const kEnvironment =
"LaunchDarkly_rUTcjlHPv6Vegd27YmtGYkEGkEUGaEbn5M0JYTFQUpA=";
char const* const kContextId = "CEXjZY7cHJG_ydFy7q4-YEFwVrG3_pkJwA4FAjrbfx0=";

} // namespace

// The cache is the only source offline mode has, so a miss still starts the
// SDK with no flags, evaluating to defaults.
TEST(FDv2ClientTest, OfflineModeStartsWithNoCachedData) {
Client client(OfflineFDv2Config(nullptr),
ContextBuilder().Kind("user", "user-key").Build());

auto started = client.StartAsync();

ASSERT_EQ(std::future_status::ready, started.wait_for(5s));
EXPECT_TRUE(started.get());
EXPECT_TRUE(client.Initialized());
EXPECT_TRUE(client.AllFlags().empty());
}

TEST(FDv2ClientTest, OfflineModeEvaluatesAgainstTheCache) {
auto persistence =
std::make_shared<TestPersistence>(TestPersistence::StoreType{
{kEnvironment,
{{kContextId, R"({"treat":{"version":1,"value":"fish"}})"}}}});

Client client(OfflineFDv2Config(persistence),
ContextBuilder().Kind("user", "user-key").Build());

auto started = client.StartAsync();

ASSERT_EQ(std::future_status::ready, started.wait_for(5s));
EXPECT_TRUE(started.get());
EXPECT_EQ("fish", client.StringVariation("treat", "chicken"));
}
Loading
Loading