From 2eda0b734c77f165cc53d5e63e9f39b504282bbc Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 24 Apr 2023 15:41:55 -0700 Subject: [PATCH 01/14] feat: Implement polling data source. --- apps/hello-cpp/main.cpp | 19 ++- ...dler.hpp => data_source_event_handler.hpp} | 22 +-- .../detail/polling_data_source.hpp | 61 +++++++++ .../detail/streaming_data_source.hpp | 4 +- libs/client-sdk/src/CMakeLists.txt | 3 +- libs/client-sdk/src/api.cpp | 47 +++++-- ...dler.cpp => data_source_event_handler.cpp} | 64 ++++----- .../src/data_sources/polling_data_source.cpp | 125 ++++++++++++++++++ .../data_sources/streaming_data_source.cpp | 4 +- ...cpp => data_source_event_handler_test.cpp} | 104 +++++++-------- .../builders/http_properties_builder.hpp | 2 +- .../include/network/detail/asio_requester.hpp | 4 + .../include/network/detail/http_requester.hpp | 2 + .../src/config/http_properties_builder.cpp | 2 +- libs/common/src/network/http_requester.cpp | 11 +- 15 files changed, 356 insertions(+), 118 deletions(-) rename libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/{streaming_data_handler.hpp => data_source_event_handler.hpp} (67%) create mode 100644 libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp rename libs/client-sdk/src/data_sources/{streaming_data_handler.cpp => data_source_event_handler.cpp} (73%) create mode 100644 libs/client-sdk/src/data_sources/polling_data_source.cpp rename libs/client-sdk/tests/{streaming_data_handler_test.cpp => data_source_event_handler_test.cpp} (59%) diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index 555a3bcab..a5c148825 100644 --- a/apps/hello-cpp/main.cpp +++ b/apps/hello-cpp/main.cpp @@ -3,6 +3,7 @@ #include +#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" @@ -17,6 +18,7 @@ using launchdarkly::Logger; using launchdarkly::LogLevel; using launchdarkly::client_side::Client; using launchdarkly::client_side::ConfigBuilder; +using launchdarkly::client_side::DataSourceBuilder; using launchdarkly::client_side::flag_manager::detail::FlagManager; using launchdarkly::client_side::flag_manager::detail::FlagUpdater; @@ -31,11 +33,24 @@ int main() { return 1; } - Client client(ConfigBuilder(key).Build().value(), - ContextBuilder().kind("user", "ryan").build()); + Client client( + ConfigBuilder(key) + .DataSource(DataSourceBuilder() + .Method(DataSourceBuilder::Polling().PollInterval( + std::chrono::seconds{30})) + .WithReasons(true) + .UseReport(true)) + .Build() + .value(), + ContextBuilder().kind("user", "ryan").build()); client.WaitForReadySync(std::chrono::seconds(30)); auto value = client.BoolVariation("my-boolean-flag", false); LD_LOG(logger, LogLevel::kInfo) << "Value was: " << value; + + // Sit around. + std::string t; + std::cout << "Press enter to exit" << std::endl; + std::cin >> t; } diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/streaming_data_handler.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/data_source_event_handler.hpp similarity index 67% rename from libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/streaming_data_handler.hpp rename to libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/data_source_event_handler.hpp index 2d399242e..70baa8215 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/streaming_data_handler.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/data_source_event_handler.hpp @@ -8,16 +8,18 @@ #include "launchdarkly/client_side/data_source.hpp" #include "launchdarkly/client_side/data_source_update_sink.hpp" #include "launchdarkly/client_side/data_sources/detail/data_source_status_manager.hpp" -#include "launchdarkly/sse/client.hpp" #include "logger.hpp" namespace launchdarkly::client_side::data_sources::detail { /** - * This class handles events source events, parses them, and then uses + * This class handles LaunchDarkly events, parses them, and then uses * a IDataSourceUpdateSink to process the parsed events. + * + * This can be used for streaming or for polling. For polling only "put" events + * will be used. */ -class StreamingDataHandler { +class DataSourceEventHandler { public: /** * Status indicating if the message was processed, or if there @@ -45,16 +47,18 @@ class StreamingDataHandler { uint64_t version; }; - StreamingDataHandler(IDataSourceUpdateSink* handler, - Logger const& logger, - DataSourceStatusManager& status_manager); + DataSourceEventHandler(IDataSourceUpdateSink* handler, + Logger const& logger, + DataSourceStatusManager& status_manager); /** - * Handle an SSE event. - * @param event The event to handle. + * Handles an event from the LaunchDarkly service. + * @param type The type of the event. "put"/"patch"/"delete". + * @param data The content of the evnet. * @return A status indicating if the message could be handled. */ - MessageStatus HandleMessage(launchdarkly::sse::Event const& event); + MessageStatus HandleMessage(std::string const& type, + std::string const& data); private: IDataSourceUpdateSink* handler_; diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp new file mode 100644 index 000000000..45d05cb2b --- /dev/null +++ b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include + +#include + +#include "config/detail/built/http_properties.hpp" +#include "data_source_status_manager.hpp" +#include "launchdarkly/client_side/data_source.hpp" +#include "launchdarkly/client_side/data_source_update_sink.hpp" +#include "launchdarkly/client_side/data_sources/detail/data_source_event_handler.hpp" +#include "logger.hpp" +#include "network/detail/asio_requester.hpp" + +namespace launchdarkly::client_side::data_sources::detail { + +const static std::chrono::seconds kMinPollingInterval = + std::chrono::seconds{30}; + +class PollingDataSource : public IDataSource { + public: + PollingDataSource( + std::string const& sdk_key, + boost::asio::any_io_executor ioc, + Context const& context, + config::detail::built::ServiceEndpoints const& endpoints, + config::detail::built::HttpProperties const& http_properties, + std::chrono::seconds polling_interval, + bool use_report, + bool with_reasons, + IDataSourceUpdateSink* handler, + DataSourceStatusManager& status_manager, + Logger const& logger); + + void Start() override; + void Close() override; + + private: + void DoPoll(); + + std::string string_context_; + DataSourceStatusManager& status_manager_; + DataSourceEventHandler data_source_handler_; + std::string polling_endpoint_; + + network::detail::AsioRequester requester_; + Logger const& logger_; + boost::asio::any_io_executor ioc_; + std::chrono::seconds polling_interval_; + network::detail::HttpRequest request_; + + // TODO: Unique/Shared ptr. + boost::asio::deadline_timer* timer_; + + inline const static std::string polling_get_path_ = "/msdk/evalx/contexts"; + + inline const static std::string polling_report_path_ = + "/msdk/evalx/context"; +}; + +} // namespace launchdarkly::client_side::data_sources::detail \ No newline at end of file diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/streaming_data_source.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/streaming_data_source.hpp index 55af00cbb..64d0cdf3d 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/streaming_data_source.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/streaming_data_source.hpp @@ -11,8 +11,8 @@ using namespace std::chrono_literals; #include "data/evaluation_result.hpp" #include "launchdarkly/client_side/data_source.hpp" #include "launchdarkly/client_side/data_source_update_sink.hpp" +#include "launchdarkly/client_side/data_sources/detail/data_source_event_handler.hpp" #include "launchdarkly/client_side/data_sources/detail/data_source_status_manager.hpp" -#include "launchdarkly/client_side/data_sources/detail/streaming_data_handler.hpp" #include "launchdarkly/sse/client.hpp" #include "logger.hpp" @@ -37,7 +37,7 @@ class StreamingDataSource final : public IDataSource { private: DataSourceStatusManager& status_manager_; - StreamingDataHandler data_source_handler_; + DataSourceEventHandler data_source_handler_; std::string streaming_endpoint_; std::string string_context_; diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index b799c1fe9..aa849e3dc 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -15,8 +15,9 @@ add_library(${LIBNAME} ${HEADER_LIST} data_sources/streaming_data_source.cpp data_sources/base_64.cpp - data_sources/streaming_data_handler.cpp + data_sources/data_source_event_handler.cpp data_source_update_sink.cpp + data_sources/polling_data_source.cpp flag_manager/flag_manager.cpp flag_manager/flag_updater.cpp flag_manager/flag_change_event.cpp diff --git a/libs/client-sdk/src/api.cpp b/libs/client-sdk/src/api.cpp index 1f0b3cce7..1045c11c9 100644 --- a/libs/client-sdk/src/api.cpp +++ b/libs/client-sdk/src/api.cpp @@ -4,12 +4,41 @@ #include #include "events/detail/asio_event_processor.hpp" +#include "launchdarkly/client_side/data_sources/detail/polling_data_source.hpp" #include "launchdarkly/client_side/data_sources/detail/streaming_data_source.hpp" namespace launchdarkly::client_side { using launchdarkly::client_side::data_sources::DataSourceStatus; +static std::unique_ptr MakeDataSource( + Config const& config, + Context const& context, + boost::asio::any_io_executor executor, + flag_manager::detail::FlagUpdater& flag_updater, + data_sources::detail::DataSourceStatusManager& status_manager, + Logger& logger) { + if (config.DataSourceConfig().method.which() == 0) { + // TODO: use initial reconnect delay. + return std::make_unique( + config.SdkKey(), executor, context, config.ServiceEndpoints(), + config.HttpProperties(), config.DataSourceConfig().use_report, + config.DataSourceConfig().with_reasons, &flag_updater, + status_manager, logger); + } else { + auto polling_config = boost::get( + config.DataSourceConfig().method); + return std::make_unique< + launchdarkly::client_side::data_sources::detail::PollingDataSource>( + config.SdkKey(), executor, context, config.ServiceEndpoints(), + config.HttpProperties(), polling_config.poll_interval, + config.DataSourceConfig().use_report, + config.DataSourceConfig().with_reasons, &flag_updater, + status_manager, logger); + } +} + Client::Client(Config config, Context context) : logger_(config.Logger()), context_(std::move(context)), @@ -22,18 +51,12 @@ Client::Client(Config config, Context context) logger_)), flag_updater_(flag_manager_), // TODO: Support polling. - data_source_(std::make_unique( - config.SdkKey(), - ioc_.get_executor(), - context_, - config.ServiceEndpoints(), - config.HttpProperties(), - config.DataSourceConfig().use_report, - config.DataSourceConfig().with_reasons, - &flag_updater_, - status_manager_, - logger_)), + data_source_(MakeDataSource(config, + context_, + ioc_.get_executor(), + flag_updater_, + status_manager_, + logger_)), initialized_(false) { data_source_->Start(); diff --git a/libs/client-sdk/src/data_sources/streaming_data_handler.cpp b/libs/client-sdk/src/data_sources/data_source_event_handler.cpp similarity index 73% rename from libs/client-sdk/src/data_sources/streaming_data_handler.cpp rename to libs/client-sdk/src/data_sources/data_source_event_handler.cpp index 68f732977..4a03e76bf 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_handler.cpp +++ b/libs/client-sdk/src/data_sources/data_source_event_handler.cpp @@ -1,4 +1,4 @@ -#include "launchdarkly/client_side/data_sources/detail/streaming_data_handler.hpp" +#include "launchdarkly/client_side/data_sources/detail/data_source_event_handler.hpp" #include "launchdarkly/client_side/data_sources/detail/base_64.hpp" #include "serialization/json_evaluation_result.hpp" #include "serialization/value_mapping.hpp" @@ -56,9 +56,9 @@ tag_invoke(boost::json::value_to_tag tag_invoke( - boost::json::value_to_tag< - tl::expected> const& unused, +static tl::expected tag_invoke( + boost::json::value_to_tag> const& unused, boost::json::value const& json_value) { boost::ignore_unused(unused); @@ -71,14 +71,15 @@ static tl::expected tag_invoke( json_value); if (result.has_value() && key.has_value()) { - return StreamingDataHandler::PatchData{key.value(), result.value()}; + return DataSourceEventHandler::PatchData{key.value(), + result.value()}; } } return tl::unexpected(JsonError::kSchemaFailure); } -static tl::expected tag_invoke( - boost::json::value_to_tag tag_invoke( + boost::json::value_to_tag> const& unused, boost::json::value const& json_value) { boost::ignore_unused(unused); @@ -92,30 +93,31 @@ static tl::expected tag_invoke( ValueAsOpt(version_iter, obj.end()); if (key.has_value() && version.has_value()) { - return StreamingDataHandler::DeleteData{key.value(), - version.value()}; + return DataSourceEventHandler::DeleteData{key.value(), + version.value()}; } } return tl::unexpected(JsonError::kSchemaFailure); } -StreamingDataHandler::StreamingDataHandler( +DataSourceEventHandler::DataSourceEventHandler( IDataSourceUpdateSink* handler, Logger const& logger, DataSourceStatusManager& status_manager) : handler_(handler), logger_(logger), status_manager_(status_manager) {} -StreamingDataHandler::MessageStatus StreamingDataHandler::HandleMessage( - launchdarkly::sse::Event const& event) { - if (event.type() == "put") { +DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( + std::string const& type, + std::string const& data) { + if (type == "put") { boost::json::error_code error_code; - auto parsed = boost::json::parse(event.data(), error_code); + auto parsed = boost::json::parse(data, error_code); if (error_code) { LD_LOG(logger_, LogLevel::kError) << kErrorParsingPut; status_manager_.SetError( DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, kErrorParsingPut); - return StreamingDataHandler::MessageStatus::kInvalidMessage; + return DataSourceEventHandler::MessageStatus::kInvalidMessage; } auto res = boost::json::value_to, JsonError>>( @@ -124,62 +126,62 @@ StreamingDataHandler::MessageStatus StreamingDataHandler::HandleMessage( if (res.has_value()) { handler_->Init(res.value()); status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); - return StreamingDataHandler::MessageStatus::kMessageHandled; + return DataSourceEventHandler::MessageStatus::kMessageHandled; } LD_LOG(logger_, LogLevel::kError) << kErrorPutInvalid; status_manager_.SetError( DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, kErrorPutInvalid); - return StreamingDataHandler::MessageStatus::kInvalidMessage; + return DataSourceEventHandler::MessageStatus::kInvalidMessage; } - if (event.type() == "patch") { + if (type == "patch") { boost::json::error_code error_code; - auto parsed = boost::json::parse(event.data(), error_code); + auto parsed = boost::json::parse(data, error_code); if (error_code) { LD_LOG(logger_, LogLevel::kError) << kErrorParsingPatch; status_manager_.SetError( DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, kErrorParsingPatch); - return StreamingDataHandler::MessageStatus::kInvalidMessage; + return DataSourceEventHandler::MessageStatus::kInvalidMessage; } auto res = boost::json::value_to< - tl::expected>(parsed); + tl::expected>(parsed); if (res.has_value()) { handler_->Upsert( res.value().key, launchdarkly::client_side::ItemDescriptor(res.value().flag)); - return StreamingDataHandler::MessageStatus::kMessageHandled; + return DataSourceEventHandler::MessageStatus::kMessageHandled; } LD_LOG(logger_, LogLevel::kError) << kErrorPatchInvalid; status_manager_.SetError( DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, kErrorPatchInvalid); - return StreamingDataHandler::MessageStatus::kInvalidMessage; + return DataSourceEventHandler::MessageStatus::kInvalidMessage; } - if (event.type() == "delete") { + if (type == "delete") { boost::json::error_code error_code; - auto parsed = boost::json::parse(event.data(), error_code); + auto parsed = boost::json::parse(data, error_code); if (error_code) { LD_LOG(logger_, LogLevel::kError) << kErrorParsingDelete; status_manager_.SetError( DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, kErrorParsingDelete); - return StreamingDataHandler::MessageStatus::kInvalidMessage; + return DataSourceEventHandler::MessageStatus::kInvalidMessage; } auto res = boost::json::value_to< - tl::expected>( - boost::json::parse(event.data())); + tl::expected>( + boost::json::parse(data)); if (res.has_value()) { handler_->Upsert(res.value().key, ItemDescriptor(res.value().version)); - return StreamingDataHandler::MessageStatus::kMessageHandled; + return DataSourceEventHandler::MessageStatus::kMessageHandled; } LD_LOG(logger_, LogLevel::kError) << kErrorDeleteInvalid; status_manager_.SetError( DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, kErrorDeleteInvalid); - return StreamingDataHandler::MessageStatus::kInvalidMessage; + return DataSourceEventHandler::MessageStatus::kInvalidMessage; } - return StreamingDataHandler::MessageStatus::kUnhandledVerb; + return DataSourceEventHandler::MessageStatus::kUnhandledVerb; } } // namespace launchdarkly::client_side::data_sources::detail diff --git a/libs/client-sdk/src/data_sources/polling_data_source.cpp b/libs/client-sdk/src/data_sources/polling_data_source.cpp new file mode 100644 index 000000000..f914da5de --- /dev/null +++ b/libs/client-sdk/src/data_sources/polling_data_source.cpp @@ -0,0 +1,125 @@ +#include +#include + +#include "config/detail/builders/http_properties_builder.hpp" +#include "config/detail/sdks.hpp" +#include "launchdarkly/client_side/data_sources/detail/base_64.hpp" +#include "launchdarkly/client_side/data_sources/detail/polling_data_source.hpp" +#include "serialization/json_context.hpp" + +namespace launchdarkly::client_side::data_sources::detail { + +static network::detail::HttpRequest MakeRequest( + std::string const& sdk_key, + Context const& context, + config::detail::built::HttpProperties const& http_properties, + config::detail::built::ServiceEndpoints const& endpoints, + bool use_report, + bool with_reasons, + std::string const& polling_report_path, + std::string const& polling_get_path) { + std::string url = endpoints.PollingBaseUrl(); + + auto string_context = + boost::json::serialize(boost::json::value_from(context)); + + // TODO: Handle slashes. + + network::detail::HttpRequest::BodyType body; + network::detail::HttpMethod method = network::detail::HttpMethod::kGet; + + if (use_report) { + url.append(polling_report_path); + method = network::detail::HttpMethod::kReport; + body = string_context; + } else { + url.append(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)); + } + + if (with_reasons) { + url.append("?withReasons=true"); + } + + config::detail::builders::HttpPropertiesBuilder + builder(http_properties); + + builder.Header("authorization", sdk_key); + + return network::detail::HttpRequest(url, method, builder.Build(), body); +} + +PollingDataSource::PollingDataSource( + std::string const& sdk_key, + boost::asio::any_io_executor ioc, + Context const& context, + config::detail::built::ServiceEndpoints const& endpoints, + config::detail::built::HttpProperties const& http_properties, + std::chrono::seconds polling_interval, + bool use_report, + bool with_reasons, + IDataSourceUpdateSink* handler, + DataSourceStatusManager& status_manager, + Logger const& logger) + : ioc_(ioc), + logger_(logger), + status_manager_(status_manager), + data_source_handler_( + DataSourceEventHandler(handler, logger, status_manager_)), + requester_(ioc), + timer_(nullptr), + polling_interval_(polling_interval), + request_(MakeRequest(sdk_key, + context, + http_properties, + endpoints, + use_report, + with_reasons, + polling_report_path_, + polling_get_path_)) {} + +void PollingDataSource::DoPoll() { + LD_LOG(logger_, LogLevel::kInfo) << "Starting poll"; + requester_.Request(request_, [this](network::detail::HttpResult res) { + LD_LOG(logger_, LogLevel::kInfo) << "Poll complete"; + if (res.IsError()) { + // TODO: Do something + } + if (res.Status() == 200 || res.Status() == 304) { + data_source_handler_.HandleMessage("put", res.Body().value()); + } else { + // TODO: Do something + } + + if (timer_) { + delete timer_; + timer_ = nullptr; + } + // TODO: Calculate interval based on request time. + timer_ = new boost::asio::deadline_timer( + ioc_, boost::posix_time::seconds{polling_interval_.count()}); + + timer_->async_wait([this](const boost::system::error_code& ec) { + if (ec) { + // TODO: Something; + return; + } + LD_LOG(logger_, LogLevel::kInfo) << "Timer elapsed"; + DoPoll(); + }); + }); +} + +void PollingDataSource::Start() { + DoPoll(); +} + +void PollingDataSource::Close() { + if (timer_) { + timer_->cancel(); + } +} +} // namespace launchdarkly::client_side::data_sources::detail diff --git a/libs/client-sdk/src/data_sources/streaming_data_source.cpp b/libs/client-sdk/src/data_sources/streaming_data_source.cpp index a718f328d..d6f54bdb8 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/client-sdk/src/data_sources/streaming_data_source.cpp @@ -28,7 +28,7 @@ StreamingDataSource::StreamingDataSource( : logger_(logger), status_manager_(status_manager), data_source_handler_( - StreamingDataHandler(handler, logger, status_manager_)) { + DataSourceEventHandler(handler, logger, status_manager_)) { auto uri_components = boost::urls::parse_uri(endpoints.StreamingBaseUrl()); // TODO: Handle parsing error? @@ -57,7 +57,7 @@ StreamingDataSource::StreamingDataSource( : boost::beast::http::verb::get); client_builder.receiver([this](launchdarkly::sse::Event const& event) { - data_source_handler_.HandleMessage(event); + data_source_handler_.HandleMessage(event.type(), event.data()); // TODO: Use the result of handle message to restart the // event source if we got bad data. }); diff --git a/libs/client-sdk/tests/streaming_data_handler_test.cpp b/libs/client-sdk/tests/data_source_event_handler_test.cpp similarity index 59% rename from libs/client-sdk/tests/streaming_data_handler_test.cpp rename to libs/client-sdk/tests/data_source_event_handler_test.cpp index 8b9466c53..644ff2109 100644 --- a/libs/client-sdk/tests/streaming_data_handler_test.cpp +++ b/libs/client-sdk/tests/data_source_event_handler_test.cpp @@ -2,7 +2,7 @@ #include "console_backend.hpp" #include "context_builder.hpp" -#include "launchdarkly/client_side/data_sources/detail/streaming_data_handler.hpp" +#include "launchdarkly/client_side/data_sources/detail/data_source_event_handler.hpp" #include #include @@ -31,13 +31,13 @@ TEST(StreamingDataHandlerTests, HandlesPutMessage) { auto logger = Logger(std::make_unique("test")); auto test_handler = std::make_unique(); DataSourceStatusManager status_manager; - StreamingDataHandler stream_handler(test_handler.get(), logger, - status_manager); + DataSourceEventHandler stream_handler(test_handler.get(), logger, + status_manager); - auto res = stream_handler.HandleMessage(launchdarkly::sse::Event( - "put", R"({"flagA": {"version":1, "value":"test"}})")); + auto res = stream_handler.HandleMessage( + "put", R"({"flagA": {"version":1, "value":"test"}})"); - EXPECT_EQ(StreamingDataHandler::MessageStatus::kMessageHandled, res); + EXPECT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, res); EXPECT_EQ(1, test_handler->count_); auto expected_put = std::unordered_map{ {"flagA", ItemDescriptor(EvaluationResult( @@ -51,13 +51,12 @@ TEST(StreamingDataHandlerTests, HandlesEmptyPutMessage) { auto logger = Logger(std::make_unique("test")); auto test_handler = std::make_unique(); DataSourceStatusManager status_manager; - StreamingDataHandler stream_handler(test_handler.get(), logger, - status_manager); + DataSourceEventHandler stream_handler(test_handler.get(), logger, + status_manager); - auto res = - stream_handler.HandleMessage(launchdarkly::sse::Event("put", "{}")); + auto res = stream_handler.HandleMessage("put", "{}"); - EXPECT_EQ(StreamingDataHandler::MessageStatus::kMessageHandled, res); + EXPECT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, res); EXPECT_EQ(1, test_handler->count_); auto expected_put = std::unordered_map(); EXPECT_EQ(expected_put, test_handler->init_data_[0]); @@ -67,13 +66,12 @@ TEST(StreamingDataHandlerTests, BadJsonPut) { auto logger = Logger(std::make_unique("test")); auto test_handler = std::make_unique(); DataSourceStatusManager status_manager; - StreamingDataHandler stream_handler(test_handler.get(), logger, - status_manager); + DataSourceEventHandler stream_handler(test_handler.get(), logger, + status_manager); - auto res = - stream_handler.HandleMessage(launchdarkly::sse::Event("put", "{sorry")); + auto res = stream_handler.HandleMessage("put", "{sorry"); - EXPECT_EQ(StreamingDataHandler::MessageStatus::kInvalidMessage, res); + EXPECT_EQ(DataSourceEventHandler::MessageStatus::kInvalidMessage, res); EXPECT_EQ(0, test_handler->count_); } @@ -81,13 +79,12 @@ TEST(StreamingDataHandlerTests, BadSchemaPut) { auto logger = Logger(std::make_unique("test")); auto test_handler = std::make_unique(); DataSourceStatusManager status_manager; - StreamingDataHandler stream_handler(test_handler.get(), logger, - status_manager); + DataSourceEventHandler stream_handler(test_handler.get(), logger, + status_manager); - auto res = stream_handler.HandleMessage( - launchdarkly::sse::Event("put", "{\"potato\": {}}")); + auto res = stream_handler.HandleMessage("put", "{\"potato\": {}}"); - EXPECT_EQ(StreamingDataHandler::MessageStatus::kInvalidMessage, res); + EXPECT_EQ(DataSourceEventHandler::MessageStatus::kInvalidMessage, res); EXPECT_EQ(0, test_handler->count_); } @@ -95,13 +92,13 @@ TEST(StreamingDataHandlerTests, HandlesPatchMessage) { auto logger = Logger(std::make_unique("test")); auto test_handler = std::make_unique(); DataSourceStatusManager status_manager; - StreamingDataHandler stream_handler(test_handler.get(), logger, - status_manager); + DataSourceEventHandler stream_handler(test_handler.get(), logger, + status_manager); - auto res = stream_handler.HandleMessage(launchdarkly::sse::Event( - "patch", R"({"key": "flagA", "version":1, "value": "test"})")); + auto res = stream_handler.HandleMessage( + "patch", R"({"key": "flagA", "version":1, "value": "test"})"); - EXPECT_EQ(StreamingDataHandler::MessageStatus::kMessageHandled, res); + EXPECT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, res); EXPECT_EQ(1, test_handler->count_); auto expected_put = std::pair{ "flagA", ItemDescriptor(EvaluationResult( @@ -115,13 +112,12 @@ TEST(StreamingDataHandlerTests, BadJsonPatch) { auto logger = Logger(std::make_unique("test")); auto test_handler = std::make_unique(); DataSourceStatusManager status_manager; - StreamingDataHandler stream_handler(test_handler.get(), logger, - status_manager); + DataSourceEventHandler stream_handler(test_handler.get(), logger, + status_manager); - auto res = stream_handler.HandleMessage( - launchdarkly::sse::Event("patch", "{sorry")); + auto res = stream_handler.HandleMessage("patch", "{sorry"); - EXPECT_EQ(StreamingDataHandler::MessageStatus::kInvalidMessage, res); + EXPECT_EQ(DataSourceEventHandler::MessageStatus::kInvalidMessage, res); EXPECT_EQ(0, test_handler->count_); } @@ -129,13 +125,12 @@ TEST(StreamingDataHandlerTests, BadSchemaPatch) { auto logger = Logger(std::make_unique("test")); auto test_handler = std::make_unique(); DataSourceStatusManager status_manager; - StreamingDataHandler stream_handler(test_handler.get(), logger, - status_manager); + DataSourceEventHandler stream_handler(test_handler.get(), logger, + status_manager); - auto res = stream_handler.HandleMessage( - launchdarkly::sse::Event("patch", R"({"potato": {}})")); + auto res = stream_handler.HandleMessage("patch", R"({"potato": {}})"); - EXPECT_EQ(StreamingDataHandler::MessageStatus::kInvalidMessage, res); + EXPECT_EQ(DataSourceEventHandler::MessageStatus::kInvalidMessage, res); EXPECT_EQ(0, test_handler->count_); } @@ -143,13 +138,13 @@ TEST(StreamingDataHandlerTests, HandlesDeleteMessage) { auto logger = Logger(std::make_unique("test")); auto test_handler = std::make_unique(); DataSourceStatusManager status_manager; - StreamingDataHandler stream_handler(test_handler.get(), logger, - status_manager); + DataSourceEventHandler stream_handler(test_handler.get(), logger, + status_manager); - auto res = stream_handler.HandleMessage( - launchdarkly::sse::Event("delete", R"({"key": "flagA", "version":1})")); + auto res = stream_handler.HandleMessage("delete", + R"({"key": "flagA", "version":1})"); - EXPECT_EQ(StreamingDataHandler::MessageStatus::kMessageHandled, res); + EXPECT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, res); EXPECT_EQ(1, test_handler->count_); auto expected_put = std::pair{"flagA", ItemDescriptor(1)}; @@ -160,13 +155,12 @@ TEST(StreamingDataHandlerTests, BadJsonDelete) { auto logger = Logger(std::make_unique("test")); auto test_handler = std::make_unique(); DataSourceStatusManager status_manager; - StreamingDataHandler stream_handler(test_handler.get(), logger, - status_manager); + DataSourceEventHandler stream_handler(test_handler.get(), logger, + status_manager); - auto res = stream_handler.HandleMessage( - launchdarkly::sse::Event("delete", "{sorry")); + auto res = stream_handler.HandleMessage("delete", "{sorry"); - EXPECT_EQ(StreamingDataHandler::MessageStatus::kInvalidMessage, res); + EXPECT_EQ(DataSourceEventHandler::MessageStatus::kInvalidMessage, res); EXPECT_EQ(0, test_handler->count_); } @@ -174,13 +168,12 @@ TEST(StreamingDataHandlerTests, BadSchemaDelete) { auto logger = Logger(std::make_unique("test")); auto test_handler = std::make_unique(); DataSourceStatusManager status_manager; - StreamingDataHandler stream_handler(test_handler.get(), logger, - status_manager); + DataSourceEventHandler stream_handler(test_handler.get(), logger, + status_manager); - auto res = stream_handler.HandleMessage( - launchdarkly::sse::Event("delete", R"({"potato": {}})")); + auto res = stream_handler.HandleMessage("delete", R"({"potato": {}})"); - EXPECT_EQ(StreamingDataHandler::MessageStatus::kInvalidMessage, res); + EXPECT_EQ(DataSourceEventHandler::MessageStatus::kInvalidMessage, res); EXPECT_EQ(0, test_handler->count_); } @@ -188,12 +181,11 @@ TEST(StreamingDataHandlerTests, UnrecognizedVerb) { auto logger = Logger(std::make_unique("test")); auto test_handler = std::make_unique(); DataSourceStatusManager status_manager; - StreamingDataHandler stream_handler(test_handler.get(), logger, - status_manager); + DataSourceEventHandler stream_handler(test_handler.get(), logger, + status_manager); - auto res = stream_handler.HandleMessage( - launchdarkly::sse::Event("potato", R"({"potato": {}})")); + auto res = stream_handler.HandleMessage("potato", R"({"potato": {}})"); - EXPECT_EQ(StreamingDataHandler::MessageStatus::kUnhandledVerb, res); + EXPECT_EQ(DataSourceEventHandler::MessageStatus::kUnhandledVerb, res); EXPECT_EQ(0, test_handler->count_); } diff --git a/libs/common/include/config/detail/builders/http_properties_builder.hpp b/libs/common/include/config/detail/builders/http_properties_builder.hpp index 44c0c1a53..69cd2a00c 100644 --- a/libs/common/include/config/detail/builders/http_properties_builder.hpp +++ b/libs/common/include/config/detail/builders/http_properties_builder.hpp @@ -38,7 +38,7 @@ class HttpPropertiesBuilder { * * @param properties The properties to start with. */ - HttpPropertiesBuilder(built::HttpProperties properties); + HttpPropertiesBuilder(built::HttpProperties const& properties); /** * The network connection timeout. diff --git a/libs/common/include/network/detail/asio_requester.hpp b/libs/common/include/network/detail/asio_requester.hpp index c95654c45..e6358556c 100644 --- a/libs/common/include/network/detail/asio_requester.hpp +++ b/libs/common/include/network/detail/asio_requester.hpp @@ -62,6 +62,10 @@ static http::request MakeBeastRequest( beast_request.set(pair.first, pair.second); } +// for(auto const& pair: request.Params()) { +// beast_request. +// } + beast_request.set(http::field::user_agent, properties.UserAgent()); return beast_request; diff --git a/libs/common/include/network/detail/http_requester.hpp b/libs/common/include/network/detail/http_requester.hpp index cb6fc0923..b68583c33 100644 --- a/libs/common/include/network/detail/http_requester.hpp +++ b/libs/common/include/network/detail/http_requester.hpp @@ -80,6 +80,7 @@ class HttpRequest { std::string const& Host() const; std::string const& Port() const; std::string const& Path() const; + std::map const& Params() const; bool Https() const; HttpRequest(std::string const& url, @@ -94,6 +95,7 @@ class HttpRequest { std::string host_; std::string port_; std::string path_; + std::map params_; bool is_https_; }; diff --git a/libs/common/src/config/http_properties_builder.cpp b/libs/common/src/config/http_properties_builder.cpp index e4e3bc53b..74e905484 100644 --- a/libs/common/src/config/http_properties_builder.cpp +++ b/libs/common/src/config/http_properties_builder.cpp @@ -12,7 +12,7 @@ HttpPropertiesBuilder::HttpPropertiesBuilder() template HttpPropertiesBuilder::HttpPropertiesBuilder( - built::HttpProperties properties) { + built::HttpProperties const& properties) { connect_timeout_ = properties.ConnectTimeout(); read_timeout_ = properties.ReadTimeout(); response_timeout_ = properties.ResponseTimeout(); diff --git a/libs/common/src/network/http_requester.cpp b/libs/common/src/network/http_requester.cpp index 3bb1356b4..57c34fe1c 100644 --- a/libs/common/src/network/http_requester.cpp +++ b/libs/common/src/network/http_requester.cpp @@ -57,6 +57,10 @@ std::string const& HttpRequest::Path() const { return path_; } +std::map const& HttpRequest::Params() const { + return params_; +} + HttpRequest::HttpRequest(std::string const& url, HttpMethod method, config::detail::built::HttpProperties properties, @@ -67,7 +71,8 @@ HttpRequest::HttpRequest(std::string const& url, auto uri_components = boost::urls::parse_uri(url); host_ = uri_components->host(); - path_ = uri_components->path(); + // For a boost beast request we need the query string in the path. + path_ = uri_components->path() + "?" + uri_components->query(); if (path_.empty()) { path_ = "/"; } @@ -78,6 +83,10 @@ HttpRequest::HttpRequest(std::string const& url, } else { port_ = is_https_ ? "443" : "80"; } + +// for (auto const& param : uri_components->params()) { +// params_.insert_or_assign(param.key, param.value); +// } } std::string const& HttpRequest::Port() const { From 34e28ad4a71e1eb50194817953156f70a00eb80f Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 25 Apr 2023 09:25:17 -0700 Subject: [PATCH 02/14] Add Etag support. --- .../detail/polling_data_source.hpp | 5 +- .../src/data_sources/polling_data_source.cpp | 73 ++++++++++++++----- .../include/network/detail/http_requester.hpp | 23 +++++- libs/common/src/network/http_requester.cpp | 17 ++++- 4 files changed, 91 insertions(+), 27 deletions(-) diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp index 45d05cb2b..558c7219d 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp @@ -48,14 +48,17 @@ class PollingDataSource : public IDataSource { boost::asio::any_io_executor ioc_; std::chrono::seconds polling_interval_; network::detail::HttpRequest request_; + std::optional etag_; // TODO: Unique/Shared ptr. - boost::asio::deadline_timer* timer_; + boost::asio::steady_timer timer_; inline const static std::string polling_get_path_ = "/msdk/evalx/contexts"; inline const static std::string polling_report_path_ = "/msdk/evalx/context"; + + void StartPollingTimer(); }; } // namespace launchdarkly::client_side::data_sources::detail \ No newline at end of file diff --git a/libs/client-sdk/src/data_sources/polling_data_source.cpp b/libs/client-sdk/src/data_sources/polling_data_source.cpp index f914da5de..661f9219a 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.cpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.cpp @@ -70,7 +70,7 @@ PollingDataSource::PollingDataSource( data_source_handler_( DataSourceEventHandler(handler, logger, status_manager_)), requester_(ioc), - timer_(nullptr), + timer_(ioc), polling_interval_(polling_interval), request_(MakeRequest(sdk_key, context, @@ -84,9 +84,45 @@ PollingDataSource::PollingDataSource( void PollingDataSource::DoPoll() { LD_LOG(logger_, LogLevel::kInfo) << "Starting poll"; requester_.Request(request_, [this](network::detail::HttpResult res) { - LD_LOG(logger_, LogLevel::kInfo) << "Poll complete"; + LD_LOG(logger_, LogLevel::kInfo) << "Poll complete: " << res; + // TODO: Retry support. + + auto header_etag = res.Headers().find("etag"); + bool has_etag = header_etag != res.Headers().end(); + + LD_LOG(logger_, LogLevel::kInfo) << "has_etag: " << has_etag; + LD_LOG(logger_, LogLevel::kInfo) + << "has local etag: " << etag_.has_value(); + + if (etag_ && has_etag) { + LD_LOG(logger_, LogLevel::kInfo) << "Comparing Etag."; + if (etag_.value() == header_etag->second) { + LD_LOG(logger_, LogLevel::kInfo) + << "Etag matched. Start next poll"; + // Got the same etag, we know the content has not changed. + // So we can just start the next timer. + + // We don't need to update the "request_" because it would have + // the same Etag. + StartPollingTimer(); + return; + } + } + + if (has_etag) { + LD_LOG(logger_, LogLevel::kInfo) << "Updating Etag" << res.Status(); + config::detail::builders::HttpPropertiesBuilder< + config::detail::ClientSDK> + builder(request_.Properties()); + builder.Header("If-None-Match", header_etag->second); + request_ = network::detail::HttpRequest(request_, builder.Build()); + + etag_ = header_etag->second; + } + if (res.IsError()) { // TODO: Do something + LD_LOG(logger_, LogLevel::kError) << "Got error result" << res; } if (res.Status() == 200 || res.Status() == 304) { data_source_handler_.HandleMessage("put", res.Body().value()); @@ -94,22 +130,21 @@ void PollingDataSource::DoPoll() { // TODO: Do something } - if (timer_) { - delete timer_; - timer_ = nullptr; + StartPollingTimer(); + }); +} +void PollingDataSource::StartPollingTimer() { + // TODO: Calculate interval based on request time. + timer_.cancel(); + timer_.expires_after(polling_interval_); + + timer_.async_wait([this](boost::system::error_code const& ec) { + if (ec) { + // TODO: Something; + return; } - // TODO: Calculate interval based on request time. - timer_ = new boost::asio::deadline_timer( - ioc_, boost::posix_time::seconds{polling_interval_.count()}); - - timer_->async_wait([this](const boost::system::error_code& ec) { - if (ec) { - // TODO: Something; - return; - } - LD_LOG(logger_, LogLevel::kInfo) << "Timer elapsed"; - DoPoll(); - }); + LD_LOG(logger_, LogLevel::kInfo) << "Timer elapsed"; + DoPoll(); }); } @@ -118,8 +153,6 @@ void PollingDataSource::Start() { } void PollingDataSource::Close() { - if (timer_) { - timer_->cancel(); - } + timer_.cancel(); } } // namespace launchdarkly::client_side::data_sources::detail diff --git a/libs/common/include/network/detail/http_requester.hpp b/libs/common/include/network/detail/http_requester.hpp index b68583c33..9cc724249 100644 --- a/libs/common/include/network/detail/http_requester.hpp +++ b/libs/common/include/network/detail/http_requester.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include @@ -10,10 +12,17 @@ namespace launchdarkly::network::detail { +struct CaseInsensitiveComparator { + bool operator()(std::string const& a, std::string const& b) const noexcept { + return ::strcasecmp(a.c_str(), b.c_str()) < 0; + } +}; + class HttpResult { public: using StatusCode = uint64_t; - using HeadersType = std::map; + using HeadersType = + std::map; using BodyType = std::optional; bool IsError() const; @@ -71,7 +80,8 @@ enum class HttpMethod { kPost, kGet, kReport, kPut }; class HttpRequest { public: - using HeadersType = std::map; + using HeadersType = + std::map; using BodyType = std::optional; HttpMethod Method() const; @@ -88,6 +98,15 @@ class HttpRequest { config::detail::built::HttpProperties properties, BodyType body); + /** + * Move the contents of the base request and create a new request + * incorporating the provided properties. + * @param base_request The base request. + * @param properties The properties for the request. + */ + HttpRequest(HttpRequest& base_request, + config::detail::built::HttpProperties properties); + private: HttpMethod method_; std::optional body_; diff --git a/libs/common/src/network/http_requester.cpp b/libs/common/src/network/http_requester.cpp index 57c34fe1c..1858729d3 100644 --- a/libs/common/src/network/http_requester.cpp +++ b/libs/common/src/network/http_requester.cpp @@ -24,7 +24,9 @@ HttpResult::HttpResult(HttpResult::StatusCode status, : status_(status), body_(std::move(body)), headers_(std::move(headers)), - error_(false) {} + error_(false) { + +} bool HttpResult::IsError() const { return error_; @@ -83,10 +85,17 @@ HttpRequest::HttpRequest(std::string const& url, } else { port_ = is_https_ ? "443" : "80"; } +} -// for (auto const& param : uri_components->params()) { -// params_.insert_or_assign(param.key, param.value); -// } +HttpRequest::HttpRequest(HttpRequest& base_request, + config::detail::built::HttpProperties properties) + : properties_(std::move(properties)), + method_(base_request.method_), + body_(std::move(base_request.body_)) { + path_ = base_request.path_; + host_ = base_request.host_; + port_ = base_request.port_; + is_https_ = base_request.is_https_; } std::string const& HttpRequest::Port() const { From 837e0816ec03d014992cde7a08703c6faf3f2a8b Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 25 Apr 2023 09:28:18 -0700 Subject: [PATCH 03/14] Remove debug logging. --- .../src/data_sources/polling_data_source.cpp | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/libs/client-sdk/src/data_sources/polling_data_source.cpp b/libs/client-sdk/src/data_sources/polling_data_source.cpp index 661f9219a..bb806492d 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.cpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.cpp @@ -82,23 +82,14 @@ PollingDataSource::PollingDataSource( polling_get_path_)) {} void PollingDataSource::DoPoll() { - LD_LOG(logger_, LogLevel::kInfo) << "Starting poll"; requester_.Request(request_, [this](network::detail::HttpResult res) { - LD_LOG(logger_, LogLevel::kInfo) << "Poll complete: " << res; // TODO: Retry support. auto header_etag = res.Headers().find("etag"); bool has_etag = header_etag != res.Headers().end(); - LD_LOG(logger_, LogLevel::kInfo) << "has_etag: " << has_etag; - LD_LOG(logger_, LogLevel::kInfo) - << "has local etag: " << etag_.has_value(); - if (etag_ && has_etag) { - LD_LOG(logger_, LogLevel::kInfo) << "Comparing Etag."; if (etag_.value() == header_etag->second) { - LD_LOG(logger_, LogLevel::kInfo) - << "Etag matched. Start next poll"; // Got the same etag, we know the content has not changed. // So we can just start the next timer. @@ -110,7 +101,6 @@ void PollingDataSource::DoPoll() { } if (has_etag) { - LD_LOG(logger_, LogLevel::kInfo) << "Updating Etag" << res.Status(); config::detail::builders::HttpPropertiesBuilder< config::detail::ClientSDK> builder(request_.Properties()); @@ -143,7 +133,6 @@ void PollingDataSource::StartPollingTimer() { // TODO: Something; return; } - LD_LOG(logger_, LogLevel::kInfo) << "Timer elapsed"; DoPoll(); }); } From 82901079b2e09ea9b5be86b0c76e04de16b41ae0 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 25 Apr 2023 15:34:38 -0700 Subject: [PATCH 04/14] Calculate polling delay. --- .../data_sources/data_source_status.hpp | 3 +- .../detail/data_source_status_manager.hpp | 28 +++++++- .../detail/polling_data_source.hpp | 5 +- .../data_source_status_manager.cpp | 67 ++++++++++++++--- .../src/data_sources/polling_data_source.cpp | 71 ++++++++++++++++--- .../tests/data_source_status_manager_test.cpp | 61 +++++++++++----- .../include/network/detail/asio_requester.hpp | 4 -- .../network/detail/http_error_messages.hpp | 23 ++++++ .../include/network/detail/http_requester.hpp | 2 + libs/common/src/CMakeLists.txt | 3 +- .../src/network/http_error_messages.cpp | 19 +++++ libs/common/src/network/http_requester.cpp | 8 ++- 12 files changed, 243 insertions(+), 51 deletions(-) create mode 100644 libs/common/include/network/detail/http_error_messages.hpp create mode 100644 libs/common/src/network/http_error_messages.cpp diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_sources/data_source_status.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_sources/data_source_status.hpp index 62c283a91..7279f507a 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/data_sources/data_source_status.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/data_sources/data_source_status.hpp @@ -10,6 +10,7 @@ #include "launchdarkly/client_side/connection.hpp" #include "launchdarkly/client_side/data_sources/data_source_status.hpp" +#include "network/detail/http_requester.hpp" namespace launchdarkly::client_side::data_sources { @@ -79,7 +80,7 @@ class DataSourceStatus { */ class ErrorInfo { public: - using StatusCodeType = int32_t; + using StatusCodeType = network::detail::HttpResult::StatusCode; /** * An enumeration describing the general type of an error. diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/data_source_status_manager.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/data_source_status_manager.hpp index 9c730fc9c..aa7e0303a 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/data_source_status_manager.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/data_source_status_manager.hpp @@ -25,6 +25,30 @@ class DataSourceStatusManager : public IDataSourceStatusProvider { */ void SetState(DataSourceStatus::DataSourceState state); + /** + * If an error and state change happen simultaneously, then they should + * be updated simultaneously. + * + * @param state The new state. + * @param code Status code for an http error. + * @param message The message to associate with the error. + */ + void SetState(DataSourceStatus::DataSourceState state, + DataSourceStatus::ErrorInfo::StatusCodeType code, + std::string message); + + /** + * If an error and state change happen simultaneously, then they should + * be updated simultaneously. + * + * @param state The new state. + * @param kind The error kind. + * @param message The message to associate with the error. + */ + void SetState(DataSourceStatus::DataSourceState state, + DataSourceStatus::ErrorInfo::ErrorKind kind, + std::string message); + /** * Set an error with the given kind and message. * @@ -42,7 +66,8 @@ class DataSourceStatusManager : public IDataSourceStatusProvider { * Set an error based on the given status code. * @param code The status code of the error. */ - void SetError(DataSourceStatus::ErrorInfo::StatusCodeType code); + void SetError(DataSourceStatus::ErrorInfo::StatusCodeType code, + std::string message); // TODO: Handle error codes once the EventSource supports it. DataSourceStatus Status() override; @@ -61,6 +86,7 @@ class DataSourceStatusManager : public IDataSourceStatusProvider { boost::signals2::signal data_source_status_signal_; mutable std::mutex status_mutex_; + bool UpdateState(DataSourceStatus::DataSourceState const& requested_state); }; } // namespace launchdarkly::client_side::data_sources::detail diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp index 558c7219d..03b09c74c 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp @@ -14,9 +14,6 @@ namespace launchdarkly::client_side::data_sources::detail { -const static std::chrono::seconds kMinPollingInterval = - std::chrono::seconds{30}; - class PollingDataSource : public IDataSource { public: PollingDataSource( @@ -50,8 +47,8 @@ class PollingDataSource : public IDataSource { network::detail::HttpRequest request_; std::optional etag_; - // TODO: Unique/Shared ptr. boost::asio::steady_timer timer_; + std::chrono::time_point last_poll_start_; inline const static std::string polling_get_path_ = "/msdk/evalx/contexts"; diff --git a/libs/client-sdk/src/data_sources/data_source_status_manager.cpp b/libs/client-sdk/src/data_sources/data_source_status_manager.cpp index 592f5c3b3..b19a48a1d 100644 --- a/libs/client-sdk/src/data_sources/data_source_status_manager.cpp +++ b/libs/client-sdk/src/data_sources/data_source_status_manager.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include "launchdarkly/client_side/connection.hpp" @@ -10,18 +11,62 @@ namespace launchdarkly::client_side::data_sources::detail { void DataSourceStatusManager::SetState( DataSourceStatus::DataSourceState state) { - bool changed = false; + bool changed = UpdateState(state); + if (changed) { + data_source_status_signal_(std::move(Status())); + } +} + +void DataSourceStatusManager::SetState( + DataSourceStatus::DataSourceState state, + DataSourceStatus::ErrorInfo::StatusCodeType code, + std::string message) { { std::lock_guard lock(status_mutex_); - changed = state_ != state; - state_ = state; - if (changed) { - state_since_ = std::chrono::system_clock::now(); - } + + UpdateState(state); + + last_error_ = DataSourceStatus::ErrorInfo( + DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, code, + message, std::chrono::system_clock::now()); } + + data_source_status_signal_(std::move(Status())); +} +bool DataSourceStatusManager::UpdateState( + DataSourceStatus::DataSourceState const& requested_state) { + std::lock_guard lock(status_mutex_); + + // If initializing, then interruptions remain initializing. + auto new_state = + (requested_state == DataSourceStatus::DataSourceState::kInterrupted && + state_ == DataSourceStatus::DataSourceState::kInitializing) + ? DataSourceStatus::DataSourceState:: + kInitializing // see comment on + // IDataSourceUpdateSink.UpdateStatus + : requested_state; + auto changed = state_ != new_state; if (changed) { - data_source_status_signal_(std::move(Status())); + state_ = new_state; + state_since_ = std::chrono::system_clock::now(); } + return changed; +} + +void DataSourceStatusManager::SetState( + DataSourceStatus::DataSourceState state, + DataSourceStatus::ErrorInfo::ErrorKind kind, + std::string message) { + { + std::lock_guard lock(status_mutex_); + + UpdateState(state); + + last_error_ = DataSourceStatus::ErrorInfo( + kind, 0, std::move(message), std::chrono::system_clock::now()); + } + + data_source_status_signal_(Status()); } void DataSourceStatusManager::SetError( @@ -38,13 +83,13 @@ void DataSourceStatusManager::SetError( } void DataSourceStatusManager::SetError( - DataSourceStatus::ErrorInfo::StatusCodeType code) { - // TODO: String message. + DataSourceStatus::ErrorInfo::StatusCodeType code, + std::string message) { { std::lock_guard lock(status_mutex_); last_error_ = DataSourceStatus::ErrorInfo( - DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, code, "", - std::chrono::system_clock::now()); + DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, code, + message, std::chrono::system_clock::now()); state_since_ = std::chrono::system_clock::now(); } data_source_status_signal_(Status()); diff --git a/libs/client-sdk/src/data_sources/polling_data_source.cpp b/libs/client-sdk/src/data_sources/polling_data_source.cpp index bb806492d..dd6270237 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.cpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.cpp @@ -5,10 +5,14 @@ #include "config/detail/sdks.hpp" #include "launchdarkly/client_side/data_sources/detail/base_64.hpp" #include "launchdarkly/client_side/data_sources/detail/polling_data_source.hpp" +#include "network/detail/http_error_messages.hpp" #include "serialization/json_context.hpp" namespace launchdarkly::client_side::data_sources::detail { +const static std::chrono::seconds kMinPollingInterval = + std::chrono::seconds{30}; + static network::detail::HttpRequest MakeRequest( std::string const& sdk_key, Context const& context, @@ -79,12 +83,20 @@ PollingDataSource::PollingDataSource( use_report, with_reasons, polling_report_path_, - polling_get_path_)) {} + polling_get_path_)) { + if (polling_interval_ < kMinPollingInterval) { + LD_LOG(logger_, LogLevel::kWarn) + << "Polling interval specified under minimum, defaulting to 30 " + "second polling interval"; + + polling_interval_ = kMinPollingInterval; + } +} void PollingDataSource::DoPoll() { - requester_.Request(request_, [this](network::detail::HttpResult res) { - // TODO: Retry support. + last_poll_start_ = std::chrono::system_clock::now(); + requester_.Request(request_, [this](network::detail::HttpResult res) { auto header_etag = res.Headers().find("etag"); bool has_etag = header_etag != res.Headers().end(); @@ -111,13 +123,34 @@ void PollingDataSource::DoPoll() { } if (res.IsError()) { - // TODO: Do something - LD_LOG(logger_, LogLevel::kError) << "Got error result" << res; - } - if (res.Status() == 200 || res.Status() == 304) { + status_manager_.SetState( + DataSourceStatus::DataSourceState::kInterrupted, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + res.ErrorMessage() ? *res.ErrorMessage() : "unknown error"); + LD_LOG(logger_, LogLevel::kWarn) + << "Polling for feature flag updates failed:" + << (res.ErrorMessage() ? *res.ErrorMessage() : "unknown error"); + } else if (res.Status() == 200) { data_source_handler_.HandleMessage("put", res.Body().value()); + } else if (res.Status() == 304) { + // This should be handled ahead of here, but if we get a 304, + // and it didn't have an etag, we still don't want to try to + // parse the body. } else { - // TODO: Do something + if (network::detail::IsRecoverableStatus(res.Status())) { + status_manager_.SetState( + DataSourceStatus::DataSourceState::kInterrupted, + res.Status(), + launchdarkly::network::detail::ErrorForStatusCode( + res.Status(), "polling request", "will retry")); + } else { + status_manager_.SetState( + DataSourceStatus::DataSourceState::kShutdown, res.Status(), + launchdarkly::network::detail::ErrorForStatusCode( + res.Status(), "polling request", std::nullopt)); + // We are giving up. Do not start a new polling request. + return; + } } StartPollingTimer(); @@ -125,13 +158,31 @@ void PollingDataSource::DoPoll() { } void PollingDataSource::StartPollingTimer() { // TODO: Calculate interval based on request time. + auto time_since_poll_seconds = + std::chrono::duration_cast( + std::chrono::system_clock::now() - last_poll_start_); + + // Calculate a delay based on the polling interval and the duration elapsed + // since the last poll. + + // Example: If the poll took 5 seconds, and the interval is 30 seconds, then + // we want to poll after 25 seconds. We do not want the interval to be + // negative, so we clamp it to 0. + auto delay = std::chrono::seconds(std::max( + polling_interval_ - time_since_poll_seconds, std::chrono::seconds(0))); + timer_.cancel(); timer_.expires_after(polling_interval_); timer_.async_wait([this](boost::system::error_code const& ec) { - if (ec) { - // TODO: Something; + if (ec == boost::asio::error::operation_aborted) { + // The timer was cancelled. Stop polling. return; + } else if (ec) { + // Something unexpected happened. Log it and continue to try + // polling. + LD_LOG(logger_, LogLevel::kError) + << "Unexpected error in polling timer: " << ec.message(); } DoPoll(); }); diff --git a/libs/client-sdk/tests/data_source_status_manager_test.cpp b/libs/client-sdk/tests/data_source_status_manager_test.cpp index 8bf63edda..f7dd2eb27 100644 --- a/libs/client-sdk/tests/data_source_status_manager_test.cpp +++ b/libs/client-sdk/tests/data_source_status_manager_test.cpp @@ -9,29 +9,56 @@ using launchdarkly::client_side::data_sources::detail::DataSourceStatusManager; class DataSourceStateParameterizedTestFixture : public ::testing::TestWithParam {}; -TEST_P(DataSourceStateParameterizedTestFixture, HandlesSettingState) { +TEST(DataSourceStatusManagerTests, + WhenInitializingInterruptedDoesNotChangeState) { DataSourceStatusManager status_manager; - status_manager.SetState(GetParam()); + status_manager.SetState(DataSourceStatus::DataSourceState::kInterrupted); - EXPECT_EQ(GetParam(), status_manager.Status().State()); + EXPECT_EQ(DataSourceStatus::DataSourceState::kInitializing, + status_manager.Status().State()); } -TEST_P(DataSourceStateParameterizedTestFixture, ProducesEventOnStateChange) { +TEST(DataSourceStatusManagerTests, + WhenInitializingInterruptedDoesNotProduceEvent) { DataSourceStatusManager status_manager; - // We start in initializing, so it doesn't produce an event. - if (GetParam() != DataSourceStatus::DataSourceState::kInitializing) { - std::atomic got_event(false); - status_manager.OnDataSourceStatusChange([&got_event](auto status) { - got_event.store(true); - EXPECT_EQ(GetParam(), status.State()); - }); + std::atomic got_event(false); + status_manager.OnDataSourceStatusChange( + [&got_event](auto status) { got_event.store(true); }); + + status_manager.SetState(DataSourceStatus::DataSourceState::kInterrupted); + + EXPECT_FALSE(got_event); +} + +TEST(DataSourceStatusManagerTests, CanTransitionToValidFromInitializing) { + DataSourceStatusManager status_manager; + + status_manager.SetState(DataSourceStatus::DataSourceState::kValid); - status_manager.SetState(GetParam()); + EXPECT_EQ(DataSourceStatus::DataSourceState::kValid, + status_manager.Status().State()); +} + +TEST(DataSourceStatusManagerTests, CanTransitionFromValidToInterrupted) { + DataSourceStatusManager status_manager; + + status_manager.SetState(DataSourceStatus::DataSourceState::kValid); + + status_manager.SetState(DataSourceStatus::DataSourceState::kInterrupted); - EXPECT_TRUE(got_event); - } + EXPECT_EQ(DataSourceStatus::DataSourceState::kInterrupted, + status_manager.Status().State()); +} + +TEST(DataSourceStatusManagerTests, CanTransitionToShutdownFromInitializing) { + DataSourceStatusManager status_manager; + + status_manager.SetState(DataSourceStatus::DataSourceState::kShutdown); + + EXPECT_EQ(DataSourceStatus::DataSourceState::kShutdown, + status_manager.Status().State()); } TEST_P(DataSourceStateParameterizedTestFixture, SameStateProducesNoEvent) { @@ -99,10 +126,12 @@ INSTANTIATE_TEST_SUITE_P( TEST(DataSourceStatusManagerTests, CanSetErrorViaStatusCode) { DataSourceStatusManager status_manager; - status_manager.SetError(404); + status_manager.SetError(404, "Bad times"); EXPECT_EQ(DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, status_manager.Status().LastError()->Kind()); EXPECT_EQ(404, status_manager.Status().LastError()->StatusCode()); + + EXPECT_EQ("Bad times", status_manager.Status().LastError()->Message()); } TEST(DataSourceStatusManagerTests, NoErrorIfNoErrorHasHappened) { @@ -137,7 +166,7 @@ TEST(DataSourceStatusManagerTests, ErrorHasTimeStamp) { DataSourceStatusManager status_manager; auto before_error_set = std::chrono::system_clock::now(); - status_manager.SetError(404); + status_manager.SetError(404, "Bad news"); auto after_error_set = std::chrono::system_clock::now(); diff --git a/libs/common/include/network/detail/asio_requester.hpp b/libs/common/include/network/detail/asio_requester.hpp index e6358556c..c95654c45 100644 --- a/libs/common/include/network/detail/asio_requester.hpp +++ b/libs/common/include/network/detail/asio_requester.hpp @@ -62,10 +62,6 @@ static http::request MakeBeastRequest( beast_request.set(pair.first, pair.second); } -// for(auto const& pair: request.Params()) { -// beast_request. -// } - beast_request.set(http::field::user_agent, properties.UserAgent()); return beast_request; diff --git a/libs/common/include/network/detail/http_error_messages.hpp b/libs/common/include/network/detail/http_error_messages.hpp new file mode 100644 index 000000000..d9014716a --- /dev/null +++ b/libs/common/include/network/detail/http_error_messages.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "context.hpp" +#include "network/detail/http_requester.hpp" + +namespace launchdarkly::network::detail { + +static bool IsInvalidSdkKeyStatus(HttpResult::StatusCode code); + +/** + * Get an error message for an HTTP error. + * @param code The status code of the error. + * @param context The context of the error, for example a "polling request". + * @param retry_message The retry message, or nullopt if it will not be retried. + * @return The error message. + */ +std::string ErrorForStatusCode(HttpResult::StatusCode code, + std::string context, + std::optional retry_message); + +} // namespace launchdarkly::network::detail \ No newline at end of file diff --git a/libs/common/include/network/detail/http_requester.hpp b/libs/common/include/network/detail/http_requester.hpp index 9cc724249..0f18e1091 100644 --- a/libs/common/include/network/detail/http_requester.hpp +++ b/libs/common/include/network/detail/http_requester.hpp @@ -118,4 +118,6 @@ class HttpRequest { bool is_https_; }; +bool IsRecoverableStatus(HttpResult::StatusCode status); + } // namespace launchdarkly::network::detail diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index 1bca6f383..b437c49e6 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -45,7 +45,8 @@ add_library(${LIBNAME} config/http_properties.cpp config/data_source_builder.cpp config/http_properties_builder.cpp - network/http_requester.cpp) + network/http_requester.cpp + network/http_error_messages.cpp) add_library(launchdarkly::common ALIAS ${LIBNAME}) diff --git a/libs/common/src/network/http_error_messages.cpp b/libs/common/src/network/http_error_messages.cpp new file mode 100644 index 000000000..7c275952d --- /dev/null +++ b/libs/common/src/network/http_error_messages.cpp @@ -0,0 +1,19 @@ +#include "network/detail/http_error_messages.hpp" + +namespace launchdarkly::network::detail { + +std::string ErrorForStatusCode(HttpResult::StatusCode code, + std::string context, + std::optional retry_message) { + std::stringstream error_message; + error_message << "HTTP error " << code + << (IsInvalidSdkKeyStatus(code) ? "(invalid SDK key) " : " ") + << "for: " << context << " - " + << (retry_message ? *retry_message : "giving up permanently"); +} + +bool IsInvalidSdkKeyStatus(HttpResult::StatusCode code) { + return code == 401 || code == 403; +} + +} // namespace launchdarkly::network::detail diff --git a/libs/common/src/network/http_requester.cpp b/libs/common/src/network/http_requester.cpp index 1858729d3..40c9bf23f 100644 --- a/libs/common/src/network/http_requester.cpp +++ b/libs/common/src/network/http_requester.cpp @@ -24,9 +24,7 @@ HttpResult::HttpResult(HttpResult::StatusCode status, : status_(status), body_(std::move(body)), headers_(std::move(headers)), - error_(false) { - -} + error_(false) {} bool HttpResult::IsError() const { return error_; @@ -105,4 +103,8 @@ bool HttpRequest::Https() const { return is_https_; } +bool IsRecoverableStatus(HttpResult::StatusCode status) { + return status < 400 || status > 499 || status == 400 || status == 408 || + status == 429; +} } // namespace launchdarkly::network::detail From c023ed87b6c9e85bf9505adf1fbdfb33c5561f9f Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 25 Apr 2023 15:35:09 -0700 Subject: [PATCH 05/14] Add close call. --- libs/client-sdk/src/data_sources/polling_data_source.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/client-sdk/src/data_sources/polling_data_source.cpp b/libs/client-sdk/src/data_sources/polling_data_source.cpp index dd6270237..6fafc97e3 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.cpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.cpp @@ -183,6 +183,7 @@ void PollingDataSource::StartPollingTimer() { // polling. LD_LOG(logger_, LogLevel::kError) << "Unexpected error in polling timer: " << ec.message(); + Close(); } DoPoll(); }); From bfc9fd09b5da0686eedf08a04013efcc8bd31070 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 25 Apr 2023 15:37:59 -0700 Subject: [PATCH 06/14] Tidy --- .../client_side/data_sources/detail/polling_data_source.hpp | 2 +- libs/client-sdk/src/data_sources/polling_data_source.cpp | 6 +++--- libs/common/include/network/detail/http_requester.hpp | 1 - libs/common/src/network/http_requester.cpp | 4 ---- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp index 03b09c74c..564a01461 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp @@ -58,4 +58,4 @@ class PollingDataSource : public IDataSource { void StartPollingTimer(); }; -} // namespace launchdarkly::client_side::data_sources::detail \ No newline at end of file +} // namespace launchdarkly::client_side::data_sources::detail diff --git a/libs/client-sdk/src/data_sources/polling_data_source.cpp b/libs/client-sdk/src/data_sources/polling_data_source.cpp index 6fafc97e3..9bba0a57a 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.cpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.cpp @@ -1,5 +1,4 @@ #include -#include #include "config/detail/builders/http_properties_builder.hpp" #include "config/detail/sdks.hpp" @@ -53,7 +52,7 @@ static network::detail::HttpRequest MakeRequest( builder.Header("authorization", sdk_key); - return network::detail::HttpRequest(url, method, builder.Build(), body); + return {url, method, builder.Build(), body}; } PollingDataSource::PollingDataSource( @@ -178,7 +177,8 @@ void PollingDataSource::StartPollingTimer() { if (ec == boost::asio::error::operation_aborted) { // The timer was cancelled. Stop polling. return; - } else if (ec) { + } + if (ec) { // Something unexpected happened. Log it and continue to try // polling. LD_LOG(logger_, LogLevel::kError) diff --git a/libs/common/include/network/detail/http_requester.hpp b/libs/common/include/network/detail/http_requester.hpp index 0f18e1091..f962f76fc 100644 --- a/libs/common/include/network/detail/http_requester.hpp +++ b/libs/common/include/network/detail/http_requester.hpp @@ -90,7 +90,6 @@ class HttpRequest { std::string const& Host() const; std::string const& Port() const; std::string const& Path() const; - std::map const& Params() const; bool Https() const; HttpRequest(std::string const& url, diff --git a/libs/common/src/network/http_requester.cpp b/libs/common/src/network/http_requester.cpp index 40c9bf23f..c3801d3c0 100644 --- a/libs/common/src/network/http_requester.cpp +++ b/libs/common/src/network/http_requester.cpp @@ -57,10 +57,6 @@ std::string const& HttpRequest::Path() const { return path_; } -std::map const& HttpRequest::Params() const { - return params_; -} - HttpRequest::HttpRequest(std::string const& url, HttpMethod method, config::detail::built::HttpProperties properties, From a762fc2399e87371cbbc484d749ce483cb4bcb04 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 25 Apr 2023 15:40:03 -0700 Subject: [PATCH 07/14] Remove outdated comment. --- libs/client-sdk/src/api.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/client-sdk/src/api.cpp b/libs/client-sdk/src/api.cpp index 1045c11c9..b35a21b2e 100644 --- a/libs/client-sdk/src/api.cpp +++ b/libs/client-sdk/src/api.cpp @@ -50,7 +50,6 @@ Client::Client(Config config, Context context) config.SdkKey(), logger_)), flag_updater_(flag_manager_), - // TODO: Support polling. data_source_(MakeDataSource(config, context_, ioc_.get_executor(), From 59320874ada2961d1558c49ddcefd33450b71ca3 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 25 Apr 2023 15:42:10 -0700 Subject: [PATCH 08/14] Add a blank line. --- libs/client-sdk/src/data_sources/polling_data_source.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/client-sdk/src/data_sources/polling_data_source.cpp b/libs/client-sdk/src/data_sources/polling_data_source.cpp index 9bba0a57a..b05629124 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.cpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.cpp @@ -155,6 +155,7 @@ void PollingDataSource::DoPoll() { StartPollingTimer(); }); } + void PollingDataSource::StartPollingTimer() { // TODO: Calculate interval based on request time. auto time_since_poll_seconds = From e3c52c0f5d79b5d4950d3aec1f21ffd3d113eb47 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 26 Apr 2023 08:44:40 -0700 Subject: [PATCH 09/14] cin.get to block --- apps/hello-cpp/main.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index a5c148825..d475340e6 100644 --- a/apps/hello-cpp/main.cpp +++ b/apps/hello-cpp/main.cpp @@ -50,7 +50,6 @@ int main() { LD_LOG(logger, LogLevel::kInfo) << "Value was: " << value; // Sit around. - std::string t; std::cout << "Press enter to exit" << std::endl; - std::cin >> t; + std::cin.get(); } From 9290e1ba9e40dc8387d3d6af3bfcb158427acd73 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 26 Apr 2023 11:04:32 -0700 Subject: [PATCH 10/14] Move path and min interval to config. --- .../detail/polling_data_source.hpp | 24 ++---- libs/client-sdk/src/api.cpp | 8 +- .../src/data_sources/polling_data_source.cpp | 74 ++++++++----------- .../detail/builders/data_source_builder.hpp | 16 ++-- .../detail/built/data_source_config.hpp | 22 +++++- .../common/include/config/detail/defaults.hpp | 15 ++-- .../common/src/config/data_source_builder.cpp | 21 ++++-- .../common/tests/data_source_builder_test.cpp | 9 ++- 8 files changed, 99 insertions(+), 90 deletions(-) diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp index 564a01461..8393d038f 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp @@ -4,6 +4,7 @@ #include +#include "config/client.hpp" #include "config/detail/built/http_properties.hpp" #include "data_source_status_manager.hpp" #include "launchdarkly/client_side/data_source.hpp" @@ -16,18 +17,12 @@ namespace launchdarkly::client_side::data_sources::detail { class PollingDataSource : public IDataSource { public: - PollingDataSource( - std::string const& sdk_key, - boost::asio::any_io_executor ioc, - Context const& context, - config::detail::built::ServiceEndpoints const& endpoints, - config::detail::built::HttpProperties const& http_properties, - std::chrono::seconds polling_interval, - bool use_report, - bool with_reasons, - IDataSourceUpdateSink* handler, - DataSourceStatusManager& status_manager, - Logger const& logger); + PollingDataSource(Config const& config, + boost::asio::any_io_executor ioc, + Context const& context, + IDataSourceUpdateSink* handler, + DataSourceStatusManager& status_manager, + Logger const& logger); void Start() override; void Close() override; @@ -50,11 +45,6 @@ class PollingDataSource : public IDataSource { boost::asio::steady_timer timer_; std::chrono::time_point last_poll_start_; - inline const static std::string polling_get_path_ = "/msdk/evalx/contexts"; - - inline const static std::string polling_report_path_ = - "/msdk/evalx/context"; - void StartPollingTimer(); }; diff --git a/libs/client-sdk/src/api.cpp b/libs/client-sdk/src/api.cpp index b35a21b2e..28b9d1716 100644 --- a/libs/client-sdk/src/api.cpp +++ b/libs/client-sdk/src/api.cpp @@ -27,15 +27,9 @@ static std::unique_ptr MakeDataSource( config.DataSourceConfig().with_reasons, &flag_updater, status_manager, logger); } else { - auto polling_config = boost::get( - config.DataSourceConfig().method); return std::make_unique< launchdarkly::client_side::data_sources::detail::PollingDataSource>( - config.SdkKey(), executor, context, config.ServiceEndpoints(), - config.HttpProperties(), polling_config.poll_interval, - config.DataSourceConfig().use_report, - config.DataSourceConfig().with_reasons, &flag_updater, - status_manager, logger); + config, executor, context, &flag_updater, status_manager, logger); } } diff --git a/libs/client-sdk/src/data_sources/polling_data_source.cpp b/libs/client-sdk/src/data_sources/polling_data_source.cpp index b05629124..d91508c50 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.cpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.cpp @@ -9,19 +9,15 @@ namespace launchdarkly::client_side::data_sources::detail { -const static std::chrono::seconds kMinPollingInterval = - std::chrono::seconds{30}; - -static network::detail::HttpRequest MakeRequest( - std::string const& sdk_key, - Context const& context, - config::detail::built::HttpProperties const& http_properties, - config::detail::built::ServiceEndpoints const& endpoints, - bool use_report, - bool with_reasons, - std::string const& polling_report_path, - std::string const& polling_get_path) { - std::string url = endpoints.PollingBaseUrl(); +static network::detail::HttpRequest MakeRequest(Config const& config, + Context const& context) { + std::string url = config.ServiceEndpoints().PollingBaseUrl(); + + auto& data_source_config = config.DataSourceConfig(); + + auto& polling_config = boost::get< + config::detail::built::PollingConfig>( + config.DataSourceConfig().method); auto string_context = boost::json::serialize(boost::json::value_from(context)); @@ -31,42 +27,36 @@ static network::detail::HttpRequest MakeRequest( network::detail::HttpRequest::BodyType body; network::detail::HttpMethod method = network::detail::HttpMethod::kGet; - if (use_report) { - url.append(polling_report_path); + if (data_source_config.use_report) { + url.append(polling_config.polling_report_path); method = network::detail::HttpMethod::kReport; body = string_context; } else { - url.append(polling_get_path); + url.append(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)); } - if (with_reasons) { + if (data_source_config.with_reasons) { url.append("?withReasons=true"); } config::detail::builders::HttpPropertiesBuilder - builder(http_properties); + builder(config.HttpProperties()); - builder.Header("authorization", sdk_key); + builder.Header("authorization", config.SdkKey()); return {url, method, builder.Build(), body}; } -PollingDataSource::PollingDataSource( - std::string const& sdk_key, - boost::asio::any_io_executor ioc, - Context const& context, - config::detail::built::ServiceEndpoints const& endpoints, - config::detail::built::HttpProperties const& http_properties, - std::chrono::seconds polling_interval, - bool use_report, - bool with_reasons, - IDataSourceUpdateSink* handler, - DataSourceStatusManager& status_manager, - Logger const& logger) +PollingDataSource::PollingDataSource(Config const& config, + boost::asio::any_io_executor ioc, + Context const& context, + IDataSourceUpdateSink* handler, + DataSourceStatusManager& status_manager, + Logger const& logger) : ioc_(ioc), logger_(logger), status_manager_(status_manager), @@ -74,21 +64,21 @@ PollingDataSource::PollingDataSource( DataSourceEventHandler(handler, logger, status_manager_)), requester_(ioc), timer_(ioc), - polling_interval_(polling_interval), - request_(MakeRequest(sdk_key, - context, - http_properties, - endpoints, - use_report, - with_reasons, - polling_report_path_, - polling_get_path_)) { - if (polling_interval_ < kMinPollingInterval) { + polling_interval_( + boost::get< + config::detail::built::PollingConfig>( + config.DataSourceConfig().method) + .poll_interval), + request_(MakeRequest(config, context)) { + auto polling_config = boost::get< + config::detail::built::PollingConfig>( + config.DataSourceConfig().method); + if (polling_interval_ < polling_config.min_polling_interval) { LD_LOG(logger_, LogLevel::kWarn) << "Polling interval specified under minimum, defaulting to 30 " "second polling interval"; - polling_interval_ = kMinPollingInterval; + polling_interval_ = polling_config.min_polling_interval; } } diff --git a/libs/common/include/config/detail/builders/data_source_builder.hpp b/libs/common/include/config/detail/builders/data_source_builder.hpp index 9d9d43073..9b001dd6c 100644 --- a/libs/common/include/config/detail/builders/data_source_builder.hpp +++ b/libs/common/include/config/detail/builders/data_source_builder.hpp @@ -54,6 +54,7 @@ class StreamingBuilder { /** * Contains methods for configuring the polling data source. */ +template class PollingBuilder { public: PollingBuilder(); @@ -69,23 +70,24 @@ class PollingBuilder { * Build the polling config. Used internal to the SDK. * @return The built config. */ - [[nodiscard]] built::PollingConfig Build() const; + [[nodiscard]] built::PollingConfig Build() const; private: - built::PollingConfig config_; + built::PollingConfig config_; }; /** * The method visitor is only needed inside this file */ namespace { +template struct MethodVisitor { - boost::variant operator()( + boost::variant> operator()( StreamingBuilder streaming) { return streaming.Build(); } - boost::variant operator()( - PollingBuilder polling) { + boost::variant> operator()( + PollingBuilder polling) { return polling.Build(); } }; @@ -95,7 +97,7 @@ template <> class DataSourceBuilder { public: using Streaming = StreamingBuilder; - using Polling = PollingBuilder; + using Polling = PollingBuilder; DataSourceBuilder(); @@ -168,7 +170,7 @@ template <> class DataSourceBuilder { public: using Streaming = StreamingBuilder; - using Polling = PollingBuilder; + using Polling = PollingBuilder; DataSourceBuilder(); diff --git a/libs/common/include/config/detail/built/data_source_config.hpp b/libs/common/include/config/detail/built/data_source_config.hpp index c6f396f13..2532c5a99 100644 --- a/libs/common/include/config/detail/built/data_source_config.hpp +++ b/libs/common/include/config/detail/built/data_source_config.hpp @@ -12,7 +12,23 @@ struct StreamingConfig { std::chrono::milliseconds initial_reconnect_delay; }; -struct PollingConfig { +template +struct PollingConfig; + +template <> +struct PollingConfig { + 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}; +}; + +template <> +struct PollingConfig { std::chrono::seconds poll_interval; }; @@ -21,7 +37,7 @@ struct DataSourceConfig; template <> struct DataSourceConfig { - boost::variant method; + boost::variant> method; bool with_reasons; bool use_report; @@ -29,7 +45,7 @@ struct DataSourceConfig { template <> struct DataSourceConfig { - boost::variant method; + boost::variant> method; }; } // namespace launchdarkly::config::detail::built diff --git a/libs/common/include/config/detail/defaults.hpp b/libs/common/include/config/detail/defaults.hpp index d9aa54fdb..d45e080ea 100644 --- a/libs/common/include/config/detail/defaults.hpp +++ b/libs/common/include/config/detail/defaults.hpp @@ -24,11 +24,6 @@ struct Defaults { static auto StreamingConfig() -> built::StreamingConfig { return {std::chrono::milliseconds{1000}}; } - - static auto PollingConfig() -> built::PollingConfig { - // Default to 5 minutes; - return {std::chrono::seconds{5 * 60}}; - } }; template <> @@ -58,6 +53,11 @@ struct Defaults { static auto DataSourceConfig() -> built::DataSourceConfig { return {Defaults::StreamingConfig(), false, false}; } + + static auto PollingConfig() -> built::PollingConfig { + // Default to 5 minutes; + return {std::chrono::seconds{5 * 60}}; + } }; template <> @@ -87,6 +87,11 @@ struct Defaults { static auto DataSourceConfig() -> built::DataSourceConfig { return {Defaults::StreamingConfig()}; } + + static auto PollingConfig() -> built::PollingConfig { + // Default to 5 minutes; + return {std::chrono::seconds{5 * 60}}; + } }; } // namespace launchdarkly::config::detail diff --git a/libs/common/src/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp index 750834dad..7e6d771c6 100644 --- a/libs/common/src/config/data_source_builder.cpp +++ b/libs/common/src/config/data_source_builder.cpp @@ -15,15 +15,19 @@ built::StreamingConfig StreamingBuilder::Build() const { return config_; } -PollingBuilder::PollingBuilder() : config_(Defaults::PollingConfig()) {} +template +PollingBuilder::PollingBuilder() + : config_(Defaults::PollingConfig()) {} -PollingBuilder& PollingBuilder::PollInterval( +template +PollingBuilder& PollingBuilder::PollInterval( std::chrono::seconds poll_interval) { config_.poll_interval = poll_interval; return *this; } -built::PollingConfig PollingBuilder::Build() const { +template +built::PollingConfig PollingBuilder::Build() const { return config_; } @@ -49,13 +53,13 @@ DataSourceBuilder& DataSourceBuilder::Method( } DataSourceBuilder& DataSourceBuilder::Method( - PollingBuilder builder) { + PollingBuilder builder) { method_ = builder; return *this; } built::DataSourceConfig DataSourceBuilder::Build() const { - auto method = boost::apply_visitor(MethodVisitor(), method_); + auto method = boost::apply_visitor(MethodVisitor(), method_); return {method, with_reasons_, use_report_}; } @@ -69,14 +73,17 @@ DataSourceBuilder& DataSourceBuilder::Method( } DataSourceBuilder& DataSourceBuilder::Method( - PollingBuilder builder) { + PollingBuilder builder) { method_ = builder; return *this; } built::DataSourceConfig DataSourceBuilder::Build() const { - auto method = boost::apply_visitor(MethodVisitor(), method_); + auto method = boost::apply_visitor(MethodVisitor(), method_); return {method}; } +template class PollingBuilder; +template class PollingBuilder; + } // namespace launchdarkly::config::detail::builders diff --git a/libs/common/tests/data_source_builder_test.cpp b/libs/common/tests/data_source_builder_test.cpp index 773776dac..4ce4b5ab9 100644 --- a/libs/common/tests/data_source_builder_test.cpp +++ b/libs/common/tests/data_source_builder_test.cpp @@ -1,5 +1,6 @@ #include #include "config/client.hpp" +#include "config/detail/sdks.hpp" #include "config/server.hpp" #include "value.hpp" @@ -37,7 +38,9 @@ TEST(DataSourceBuilderTests, CanCreatePollingClientConfig) { EXPECT_FALSE(client_config.with_reasons); EXPECT_EQ( std::chrono::seconds{88000}, - boost::get(client_config.method) + boost::get< + config::detail::built::PollingConfig>( + client_config.method) .poll_interval); } @@ -63,6 +66,8 @@ TEST(DataSourceBuilderTests, CanCreatePollingServerConfig) { EXPECT_EQ( std::chrono::seconds{30000}, - boost::get(server_config.method) + boost::get< + config::detail::built::PollingConfig>( + server_config.method) .poll_interval); } From 19d93b9a691f38a7cc37c8e218a32c481aeba67a Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 26 Apr 2023 11:06:17 -0700 Subject: [PATCH 11/14] Add missing newline to end of file. --- libs/common/include/network/detail/http_error_messages.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/include/network/detail/http_error_messages.hpp b/libs/common/include/network/detail/http_error_messages.hpp index d9014716a..17b1fc35c 100644 --- a/libs/common/include/network/detail/http_error_messages.hpp +++ b/libs/common/include/network/detail/http_error_messages.hpp @@ -20,4 +20,4 @@ std::string ErrorForStatusCode(HttpResult::StatusCode code, std::string context, std::optional retry_message); -} // namespace launchdarkly::network::detail \ No newline at end of file +} // namespace launchdarkly::network::detail From c298ac08d805c873a16ee983f09884ea2e8f899b Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 26 Apr 2023 11:08:06 -0700 Subject: [PATCH 12/14] Change the server default polling interval. --- libs/common/include/config/detail/defaults.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/include/config/detail/defaults.hpp b/libs/common/include/config/detail/defaults.hpp index d45e080ea..5ee27fb88 100644 --- a/libs/common/include/config/detail/defaults.hpp +++ b/libs/common/include/config/detail/defaults.hpp @@ -90,7 +90,7 @@ struct Defaults { static auto PollingConfig() -> built::PollingConfig { // Default to 5 minutes; - return {std::chrono::seconds{5 * 60}}; + return {std::chrono::seconds{30}}; } }; From 93e7ff877c8ccae466b5c34e1120e7a13a5f35a2 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 26 Apr 2023 11:08:44 -0700 Subject: [PATCH 13/14] Remove incorrect comment. --- libs/common/include/config/detail/defaults.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/common/include/config/detail/defaults.hpp b/libs/common/include/config/detail/defaults.hpp index 5ee27fb88..947d783d9 100644 --- a/libs/common/include/config/detail/defaults.hpp +++ b/libs/common/include/config/detail/defaults.hpp @@ -89,7 +89,6 @@ struct Defaults { } static auto PollingConfig() -> built::PollingConfig { - // Default to 5 minutes; return {std::chrono::seconds{30}}; } }; From 40c58f63ceadbfbc2083745e65f5f4715d4f442d Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 26 Apr 2023 11:23:37 -0700 Subject: [PATCH 14/14] Refactor streaming config. --- .../detail/streaming_data_source.hpp | 20 ++++------- libs/client-sdk/src/api.cpp | 5 +-- .../src/data_sources/polling_data_source.cpp | 2 +- .../data_sources/streaming_data_source.cpp | 34 ++++++++++++------- .../detail/builders/data_source_builder.hpp | 15 ++++---- .../detail/built/data_source_config.hpp | 17 ++++++++-- .../common/include/config/detail/defaults.hpp | 16 +++++---- .../common/src/config/data_source_builder.cpp | 18 ++++++---- .../src/network/http_error_messages.cpp | 1 + libs/common/tests/config_builder_test.cpp | 12 ++++--- .../common/tests/data_source_builder_test.cpp | 8 +++-- 11 files changed, 89 insertions(+), 59 deletions(-) diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/streaming_data_source.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/streaming_data_source.hpp index 64d0cdf3d..52bcb0032 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/streaming_data_source.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/streaming_data_source.hpp @@ -5,6 +5,7 @@ using namespace std::chrono_literals; #include +#include "config/client.hpp" #include "config/detail/built/http_properties.hpp" #include "config/detail/built/service_endpoints.hpp" #include "context.hpp" @@ -20,17 +21,12 @@ namespace launchdarkly::client_side::data_sources::detail { class StreamingDataSource final : public IDataSource { public: - StreamingDataSource( - std::string const& sdk_key, - boost::asio::any_io_executor ioc, - Context const& context, - config::detail::built::ServiceEndpoints const& endpoints, - config::detail::built::HttpProperties const& http_properties, - bool use_report, - bool with_reasons, - IDataSourceUpdateSink* handler, - DataSourceStatusManager& status_manager, - Logger const& logger); + StreamingDataSource(Config const& config, + boost::asio::any_io_executor ioc, + Context const& context, + IDataSourceUpdateSink* handler, + DataSourceStatusManager& status_manager, + Logger const& logger); void Start() override; void Close() override; @@ -43,7 +39,5 @@ class StreamingDataSource final : public IDataSource { Logger const& logger_; std::shared_ptr client_; - - inline static const std::string streaming_path_ = "/meval"; }; } // namespace launchdarkly::client_side::data_sources::detail diff --git a/libs/client-sdk/src/api.cpp b/libs/client-sdk/src/api.cpp index 28b9d1716..e2413a9d6 100644 --- a/libs/client-sdk/src/api.cpp +++ b/libs/client-sdk/src/api.cpp @@ -22,10 +22,7 @@ static std::unique_ptr MakeDataSource( // TODO: use initial reconnect delay. return std::make_unique( - config.SdkKey(), executor, context, config.ServiceEndpoints(), - config.HttpProperties(), config.DataSourceConfig().use_report, - config.DataSourceConfig().with_reasons, &flag_updater, - status_manager, logger); + config, executor, context, &flag_updater, status_manager, logger); } else { return std::make_unique< launchdarkly::client_side::data_sources::detail::PollingDataSource>( diff --git a/libs/client-sdk/src/data_sources/polling_data_source.cpp b/libs/client-sdk/src/data_sources/polling_data_source.cpp index d91508c50..bde21c300 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.cpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.cpp @@ -70,7 +70,7 @@ PollingDataSource::PollingDataSource(Config const& config, config.DataSourceConfig().method) .poll_interval), request_(MakeRequest(config, context)) { - auto polling_config = boost::get< + auto& polling_config = boost::get< config::detail::built::PollingConfig>( config.DataSourceConfig().method); if (polling_interval_ < polling_config.min_polling_interval) { diff --git a/libs/client-sdk/src/data_sources/streaming_data_source.cpp b/libs/client-sdk/src/data_sources/streaming_data_source.cpp index d6f54bdb8..8984b47e2 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/client-sdk/src/data_sources/streaming_data_source.cpp @@ -15,13 +15,9 @@ namespace launchdarkly::client_side::data_sources::detail { StreamingDataSource::StreamingDataSource( - std::string const& sdk_key, + Config const& config, boost::asio::any_io_executor ioc, Context const& context, - config::detail::built::ServiceEndpoints const& endpoints, - config::detail::built::HttpProperties const& http_properties, - bool use_report, - bool with_reasons, IDataSourceUpdateSink* handler, DataSourceStatusManager& status_manager, Logger const& logger) @@ -29,32 +25,41 @@ StreamingDataSource::StreamingDataSource( status_manager_(status_manager), data_source_handler_( DataSourceEventHandler(handler, logger, status_manager_)) { - auto uri_components = boost::urls::parse_uri(endpoints.StreamingBaseUrl()); + 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)); + auto& data_source_config = config.DataSourceConfig(); + + auto& streaming_config = boost::get< + config::detail::built::StreamingConfig>( + data_source_config.method); + // Add the eval endpoint. - url.set_path(url.path().append(streaming_path_)); + url.set_path(url.path().append(streaming_config.streaming_path)); - if (!use_report) { + 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))); } - if (with_reasons) { + if (data_source_config.with_reasons) { url.params().set("withReasons", "true"); } auto client_builder = launchdarkly::sse::Builder(std::move(ioc), url.buffer()); - client_builder.method(use_report ? boost::beast::http::verb::report - : boost::beast::http::verb::get); + client_builder.method(data_source_config.use_report + ? boost::beast::http::verb::report + : boost::beast::http::verb::get); client_builder.receiver([this](launchdarkly::sse::Event const& event) { data_source_handler_.HandleMessage(event.type(), event.data()); @@ -65,10 +70,13 @@ StreamingDataSource::StreamingDataSource( client_builder.logger( [this](auto msg) { LD_LOG((logger_), LogLevel::kInfo) << msg; }); - if (use_report) { + if (data_source_config.use_report) { client_builder.body(string_context); } - client_builder.header("authorization", sdk_key); + + auto& http_properties = config.HttpProperties(); + + client_builder.header("authorization", config.SdkKey()); for (auto const& header : http_properties.BaseHeaders()) { client_builder.header(header.first, header.second); } diff --git a/libs/common/include/config/detail/builders/data_source_builder.hpp b/libs/common/include/config/detail/builders/data_source_builder.hpp index 9b001dd6c..df075c9cb 100644 --- a/libs/common/include/config/detail/builders/data_source_builder.hpp +++ b/libs/common/include/config/detail/builders/data_source_builder.hpp @@ -22,6 +22,7 @@ class DataSourceBuilder; /** * Builds a configuration for a streaming data source. */ +template class StreamingBuilder { public: StreamingBuilder(); @@ -45,10 +46,10 @@ class StreamingBuilder { * Build the streaming config. Used internal to the SDK. * @return The built config. */ - [[nodiscard]] built::StreamingConfig Build() const; + [[nodiscard]] built::StreamingConfig Build() const; private: - built::StreamingConfig config_; + built::StreamingConfig config_; }; /** @@ -82,11 +83,11 @@ class PollingBuilder { namespace { template struct MethodVisitor { - boost::variant> operator()( - StreamingBuilder streaming) { + boost::variant, built::PollingConfig> operator()( + StreamingBuilder streaming) { return streaming.Build(); } - boost::variant> operator()( + boost::variant, built::PollingConfig> operator()( PollingBuilder polling) { return polling.Build(); } @@ -96,7 +97,7 @@ struct MethodVisitor { template <> class DataSourceBuilder { public: - using Streaming = StreamingBuilder; + using Streaming = StreamingBuilder; using Polling = PollingBuilder; DataSourceBuilder(); @@ -169,7 +170,7 @@ class DataSourceBuilder { template <> class DataSourceBuilder { public: - using Streaming = StreamingBuilder; + using Streaming = StreamingBuilder; using Polling = PollingBuilder; DataSourceBuilder(); diff --git a/libs/common/include/config/detail/built/data_source_config.hpp b/libs/common/include/config/detail/built/data_source_config.hpp index 2532c5a99..40be3adc5 100644 --- a/libs/common/include/config/detail/built/data_source_config.hpp +++ b/libs/common/include/config/detail/built/data_source_config.hpp @@ -8,7 +8,18 @@ namespace launchdarkly::config::detail::built { -struct StreamingConfig { +template +struct StreamingConfig; + +template <> +struct StreamingConfig { + std::chrono::milliseconds initial_reconnect_delay; + + inline static const std::string streaming_path = "/meval"; +}; + +template <> +struct StreamingConfig { std::chrono::milliseconds initial_reconnect_delay; }; @@ -37,7 +48,7 @@ struct DataSourceConfig; template <> struct DataSourceConfig { - boost::variant> method; + boost::variant, PollingConfig> method; bool with_reasons; bool use_report; @@ -45,7 +56,7 @@ struct DataSourceConfig { template <> struct DataSourceConfig { - boost::variant> method; + boost::variant, PollingConfig> method; }; } // namespace launchdarkly::config::detail::built diff --git a/libs/common/include/config/detail/defaults.hpp b/libs/common/include/config/detail/defaults.hpp index 947d783d9..141a245a8 100644 --- a/libs/common/include/config/detail/defaults.hpp +++ b/libs/common/include/config/detail/defaults.hpp @@ -20,10 +20,6 @@ struct Defaults { * @return */ static bool Offline() { return false; } - - static auto StreamingConfig() -> built::StreamingConfig { - return {std::chrono::milliseconds{1000}}; - } }; template <> @@ -50,8 +46,12 @@ struct Defaults { std::map()}; } + static auto StreamingConfig() -> built::StreamingConfig { + return {std::chrono::milliseconds{1000}}; + } + static auto DataSourceConfig() -> built::DataSourceConfig { - return {Defaults::StreamingConfig(), false, false}; + return {Defaults::StreamingConfig(), false, false}; } static auto PollingConfig() -> built::PollingConfig { @@ -84,8 +84,12 @@ struct Defaults { std::map()}; } + static auto StreamingConfig() -> built::StreamingConfig { + return {std::chrono::milliseconds{1000}}; + } + static auto DataSourceConfig() -> built::DataSourceConfig { - return {Defaults::StreamingConfig()}; + return {Defaults::StreamingConfig()}; } static auto PollingConfig() -> built::PollingConfig { diff --git a/libs/common/src/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp index 7e6d771c6..9aee748b7 100644 --- a/libs/common/src/config/data_source_builder.cpp +++ b/libs/common/src/config/data_source_builder.cpp @@ -2,16 +2,19 @@ namespace launchdarkly::config::detail::builders { -StreamingBuilder::StreamingBuilder() - : config_(Defaults::StreamingConfig()) {} +template +StreamingBuilder::StreamingBuilder() + : config_(Defaults::StreamingConfig()) {} -StreamingBuilder& StreamingBuilder::InitialReconnectDelay( +template +StreamingBuilder& StreamingBuilder::InitialReconnectDelay( std::chrono::milliseconds initial_reconnect_delay) { config_.initial_reconnect_delay = initial_reconnect_delay; return *this; } -built::StreamingConfig StreamingBuilder::Build() const { +template +built::StreamingConfig StreamingBuilder::Build() const { return config_; } @@ -47,7 +50,7 @@ DataSourceBuilder& DataSourceBuilder::UseReport( } DataSourceBuilder& DataSourceBuilder::Method( - StreamingBuilder builder) { + StreamingBuilder builder) { method_ = builder; return *this; } @@ -67,7 +70,7 @@ DataSourceBuilder::DataSourceBuilder() : with_reasons_(false), use_report_(false), method_(Streaming()) {} DataSourceBuilder& DataSourceBuilder::Method( - StreamingBuilder builder) { + StreamingBuilder builder) { method_ = builder; return *this; } @@ -86,4 +89,7 @@ built::DataSourceConfig DataSourceBuilder::Build() const { template class PollingBuilder; template class PollingBuilder; +template class StreamingBuilder; +template class StreamingBuilder; + } // namespace launchdarkly::config::detail::builders diff --git a/libs/common/src/network/http_error_messages.cpp b/libs/common/src/network/http_error_messages.cpp index 7c275952d..2225ce921 100644 --- a/libs/common/src/network/http_error_messages.cpp +++ b/libs/common/src/network/http_error_messages.cpp @@ -10,6 +10,7 @@ std::string ErrorForStatusCode(HttpResult::StatusCode code, << (IsInvalidSdkKeyStatus(code) ? "(invalid SDK key) " : " ") << "for: " << context << " - " << (retry_message ? *retry_message : "giving up permanently"); + return error_message.str(); } bool IsInvalidSdkKeyStatus(HttpResult::StatusCode code) { diff --git a/libs/common/tests/config_builder_test.cpp b/libs/common/tests/config_builder_test.cpp index 493cd743e..fbfb0c3fc 100644 --- a/libs/common/tests/config_builder_test.cpp +++ b/libs/common/tests/config_builder_test.cpp @@ -73,7 +73,8 @@ TEST_F(ConfigBuilderTest, EXPECT_FALSE(cfg->DataSourceConfig().use_report); // Should be streaming with a 1 second initial retry; EXPECT_EQ(std::chrono::milliseconds{1000}, - boost::get( + boost::get>( cfg->DataSourceConfig().method) .initial_reconnect_delay); } @@ -86,7 +87,8 @@ TEST_F(ConfigBuilderTest, // Should be streaming with a 1 second initial retry; EXPECT_EQ(std::chrono::milliseconds{1000}, - boost::get( + boost::get>( cfg->DataSourceConfig().method) .initial_reconnect_delay); } @@ -102,7 +104,8 @@ TEST_F(ConfigBuilderTest, ServerConfig_CanSetDataSource) { auto cfg = builder.Build(); EXPECT_EQ(std::chrono::milliseconds{5000}, - boost::get( + boost::get>( cfg->DataSourceConfig().method) .initial_reconnect_delay); } @@ -124,7 +127,8 @@ TEST_F(ConfigBuilderTest, ClientConfig_CanSetDataSource) { EXPECT_TRUE(cfg->DataSourceConfig().use_report); EXPECT_TRUE(cfg->DataSourceConfig().with_reasons); EXPECT_EQ(std::chrono::milliseconds{5000}, - boost::get( + boost::get>( cfg->DataSourceConfig().method) .initial_reconnect_delay); } diff --git a/libs/common/tests/data_source_builder_test.cpp b/libs/common/tests/data_source_builder_test.cpp index 4ce4b5ab9..e92904112 100644 --- a/libs/common/tests/data_source_builder_test.cpp +++ b/libs/common/tests/data_source_builder_test.cpp @@ -21,7 +21,9 @@ TEST(DataSourceBuilderTests, CanCreateStreamingClientConfig) { EXPECT_TRUE(client_config.with_reasons); EXPECT_EQ( std::chrono::milliseconds{1500}, - boost::get(client_config.method) + boost::get< + config::detail::built::StreamingConfig>( + client_config.method) .initial_reconnect_delay); } @@ -53,7 +55,9 @@ TEST(DataSourceBuilderTests, CanCreateStreamingServerConfig) { EXPECT_EQ( std::chrono::milliseconds{1500}, - boost::get(server_config.method) + boost::get< + config::detail::built::StreamingConfig>( + server_config.method) .initial_reconnect_delay); }