Skip to content
7 changes: 6 additions & 1 deletion apps/hello-cpp/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

#include <boost/asio/io_context.hpp>

#include "config/detail/builders/data_source_builder.hpp"
#include "console_backend.hpp"
#include "context_builder.hpp"
#include "launchdarkly/client_side/data_sources/detail/streaming_data_source.hpp"
Expand Down Expand Up @@ -35,6 +34,12 @@ int main() {

Client client(
ConfigBuilder(key)
.ServiceEndpoints(
launchdarkly::client_side::EndpointsBuilder()
// Set to http to demonstrate redirect to https.
.PollingBaseUrl("http://sdk.launchdarkly.com")
.StreamingBaseUrl("https://stream.launchdarkly.com")
.EventsBaseUrl("https://events.launchdarkly.com"))
.DataSource(DataSourceBuilder()
.Method(DataSourceBuilder::Polling().PollInterval(
std::chrono::seconds{30}))
Expand Down
12 changes: 6 additions & 6 deletions libs/client-sdk/src/api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ static std::unique_ptr<IDataSource> MakeDataSource(
return std::make_unique<launchdarkly::client_side::data_sources::
detail::StreamingDataSource>(
config, executor, context, &flag_updater, status_manager, logger);
} else {
return std::make_unique<
launchdarkly::client_side::data_sources::detail::PollingDataSource>(
config, executor, context, &flag_updater, status_manager, logger);
}
return std::make_unique<
launchdarkly::client_side::data_sources::detail::PollingDataSource>(
config, executor, context, &flag_updater, status_manager, logger);
}

Client::Client(Config config, Context context)
Expand All @@ -48,8 +47,6 @@ Client::Client(Config config, Context context)
status_manager_,
logger_)),
initialized_(false) {
data_source_->Start();

status_manager_.OnDataSourceStatusChange([this](auto status) {
if (status.State() == DataSourceStatus::DataSourceState::kValid ||
status.State() == DataSourceStatus::DataSourceState::kShutdown ||
Expand All @@ -62,6 +59,9 @@ Client::Client(Config config, Context context)
}
});

// Should listen to status before attempting to start.
data_source_->Start();

run_thread_ = std::move(std::thread([&]() { ioc_.run(); }));
}

Expand Down
38 changes: 28 additions & 10 deletions libs/client-sdk/src/data_sources/polling_data_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@

namespace launchdarkly::client_side::data_sources::detail {

static char const* const kCouldNotParseEndpoint =
"Could not parse polling endpoint URL.";

static network::detail::HttpRequest MakeRequest(Config const& config,
Context const& context) {
std::string url = config.ServiceEndpoints().PollingBaseUrl();
auto url = std::make_optional(config.ServiceEndpoints().PollingBaseUrl());

auto& data_source_config = config.DataSourceConfig();
auto const& data_source_config = config.DataSourceConfig();

auto& polling_config = boost::get<
auto const& polling_config = boost::get<
config::detail::built::PollingConfig<config::detail::ClientSDK>>(
config.DataSourceConfig().method);

Expand All @@ -28,27 +31,32 @@ static network::detail::HttpRequest MakeRequest(Config const& config,
network::detail::HttpMethod method = network::detail::HttpMethod::kGet;

if (data_source_config.use_report) {
url.append(polling_config.polling_report_path);
url =
network::detail::AppendUrl(url, polling_config.polling_report_path);
method = network::detail::HttpMethod::kReport;
body = string_context;
} else {
url.append(polling_config.polling_get_path);
url = network::detail::AppendUrl(url, polling_config.polling_get_path);
// When not using 'REPORT' we need to base64
// encode the context so that we can safely
// put it in a url.
url.append("/" + Base64UrlEncode(string_context));
url = network::detail::AppendUrl(url, Base64UrlEncode(string_context));
}

if (data_source_config.with_reasons) {
url.append("?withReasons=true");
// TODO: Handle better.
if (url) {
url->append("?withReasons=true");
}
}

config::detail::builders::HttpPropertiesBuilder<config::detail::ClientSDK>
builder(config.HttpProperties());

builder.Header("authorization", config.SdkKey());

return {url, method, builder.Build(), body};
// If no URL is set, then we will fail the request.
return {url.value_or(""), method, builder.Build(), body};
}

PollingDataSource::PollingDataSource(Config const& config,
Expand All @@ -70,7 +78,7 @@ PollingDataSource::PollingDataSource(Config const& config,
config.DataSourceConfig().method)
.poll_interval),
request_(MakeRequest(config, context)) {
auto& polling_config = boost::get<
auto const& polling_config = boost::get<
config::detail::built::PollingConfig<config::detail::ClientSDK>>(
config.DataSourceConfig().method);
if (polling_interval_ < polling_config.min_polling_interval) {
Expand Down Expand Up @@ -147,7 +155,6 @@ void PollingDataSource::DoPoll() {
}

void PollingDataSource::StartPollingTimer() {
// TODO: Calculate interval based on request time.
auto time_since_poll_seconds =
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now() - last_poll_start_);
Expand Down Expand Up @@ -181,6 +188,17 @@ void PollingDataSource::StartPollingTimer() {
}

void PollingDataSource::Start() {
if (!request_.Valid()) {
LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint;
status_manager_.SetState(
DataSourceStatus::DataSourceState::kShutdown,
DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError,
kCouldNotParseEndpoint);

// No need to attempt to poll if the URL is not valid.
return;
}

DoPoll();
}

Expand Down
46 changes: 35 additions & 11 deletions libs/client-sdk/src/data_sources/streaming_data_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@
#include "context_builder.hpp"
#include "launchdarkly/client_side/data_sources/detail/base_64.hpp"
#include "launchdarkly/client_side/data_sources/detail/streaming_data_source.hpp"
#include "network/detail/http_requester.hpp"
#include "serialization/json_context.hpp"

namespace launchdarkly::client_side::data_sources::detail {

static char const* const kCouldNotParseEndpoint =
"Could not parse streaming endpoint URL.";

StreamingDataSource::StreamingDataSource(
Config const& config,
boost::asio::any_io_executor ioc,
Expand All @@ -25,13 +29,6 @@ StreamingDataSource::StreamingDataSource(
status_manager_(status_manager),
data_source_handler_(
DataSourceEventHandler(handler, logger, status_manager_)) {
auto uri_components =
boost::urls::parse_uri(config.ServiceEndpoints().StreamingBaseUrl());

// TODO: Handle parsing error?
// TODO: Initial reconnect delay.
boost::urls::url url = uri_components.value();

auto string_context =
boost::json::serialize(boost::json::value_from(context));

Expand All @@ -41,15 +38,32 @@ StreamingDataSource::StreamingDataSource(
config::detail::built::StreamingConfig<config::detail::ClientSDK>>(
data_source_config.method);

// Add the eval endpoint.
url.set_path(url.path().append(streaming_config.streaming_path));
auto updated_url =
network::detail::AppendUrl(config.ServiceEndpoints().StreamingBaseUrl(),
streaming_config.streaming_path);

if (!data_source_config.use_report) {
// When not using 'REPORT' we need to base64
// encode the context so that we can safely
// put it in a url.
url.set_path(url.path().append("/" + Base64UrlEncode(string_context)));
updated_url = network::detail::AppendUrl(
updated_url, Base64UrlEncode(string_context));
}
// Bad URL, don't set the client. Start will then report the bad status.
if (!updated_url) {
return;
}

auto uri_components = boost::urls::parse_uri(*updated_url);

// Unlikely that it could be parsed earlier and it cannot be parsed now.
if (!uri_components) {
return;
}

// TODO: Initial reconnect delay.
boost::urls::url url = uri_components.value();

if (data_source_config.with_reasons) {
url.params().set("withReasons", "true");
}
Expand Down Expand Up @@ -86,12 +100,22 @@ StreamingDataSource::StreamingDataSource(
}

void StreamingDataSource::Start() {
if (!client_) {
LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint;
status_manager_.SetState(
DataSourceStatus::DataSourceState::kShutdown,
DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError,
kCouldNotParseEndpoint);
return;
}
client_->run();
}

void StreamingDataSource::Close() {
status_manager_.SetState(DataSourceStatus::DataSourceState::kShutdown);
client_->close();
if (client_) {
client_->close();
}
}

} // namespace launchdarkly::client_side::data_sources::detail
13 changes: 4 additions & 9 deletions libs/common/include/config/detail/built/data_source_config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ struct StreamingConfig;
template <>
struct StreamingConfig<ClientSDK> {
std::chrono::milliseconds initial_reconnect_delay;

inline static const std::string streaming_path = "/meval";
std::string streaming_path;
};

template <>
Expand All @@ -29,13 +28,9 @@ struct PollingConfig;
template <>
struct PollingConfig<ClientSDK> {
std::chrono::seconds poll_interval;

inline const static std::string polling_get_path = "/msdk/evalx/contexts";

inline const static std::string polling_report_path = "/msdk/evalx/context";

inline const static std::chrono::seconds min_polling_interval =
std::chrono::seconds{30};
std::string polling_get_path;
std::string polling_report_path;
std::chrono::seconds min_polling_interval;
};

template <>
Expand Down
5 changes: 3 additions & 2 deletions libs/common/include/config/detail/defaults.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ struct Defaults<ClientSDK> {
}

static auto StreamingConfig() -> built::StreamingConfig<ClientSDK> {
return {std::chrono::milliseconds{1000}};
return {std::chrono::milliseconds{1000}, "/meval"};
}

static auto DataSourceConfig() -> built::DataSourceConfig<ClientSDK> {
Expand All @@ -56,7 +56,8 @@ struct Defaults<ClientSDK> {

static auto PollingConfig() -> built::PollingConfig<ClientSDK> {
// Default to 5 minutes;
return {std::chrono::seconds{5 * 60}};
return {std::chrono::seconds{5 * 60}, "/msdk/evalx/contexts",
"/msdk/evalx/context", std::chrono::seconds{30}};
}
};

Expand Down
Loading