diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index 555a3bcab..d475340e6 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,23 @@ 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::cout << "Press enter to exit" << std::endl; + std::cin.get(); } 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/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/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 new file mode 100644 index 000000000..8393d038f --- /dev/null +++ b/libs/client-sdk/include/launchdarkly/client_side/data_sources/detail/polling_data_source.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include + +#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" +#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 { + +class PollingDataSource : public IDataSource { + public: + 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; + + 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_; + std::optional etag_; + + boost::asio::steady_timer timer_; + std::chrono::time_point last_poll_start_; + + void StartPollingTimer(); +}; + +} // namespace launchdarkly::client_side::data_sources::detail 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..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,14 +5,15 @@ 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" #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" @@ -20,30 +21,23 @@ 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; private: DataSourceStatusManager& status_manager_; - StreamingDataHandler data_source_handler_; + DataSourceEventHandler data_source_handler_; std::string streaming_endpoint_; std::string string_context_; 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/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..e2413a9d6 100644 --- a/libs/client-sdk/src/api.cpp +++ b/libs/client-sdk/src/api.cpp @@ -4,12 +4,32 @@ #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, executor, context, &flag_updater, status_manager, logger); + } else { + return std::make_unique< + launchdarkly::client_side::data_sources::detail::PollingDataSource>( + config, executor, context, &flag_updater, status_manager, logger); + } +} + Client::Client(Config config, Context context) : logger_(config.Logger()), context_(std::move(context)), @@ -21,19 +41,12 @@ Client::Client(Config config, Context context) config.SdkKey(), 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/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 new file mode 100644 index 000000000..bde21c300 --- /dev/null +++ b/libs/client-sdk/src/data_sources/polling_data_source.cpp @@ -0,0 +1,190 @@ +#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 "network/detail/http_error_messages.hpp" +#include "serialization/json_context.hpp" + +namespace launchdarkly::client_side::data_sources::detail { + +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)); + + // TODO: Handle slashes. + + network::detail::HttpRequest::BodyType body; + network::detail::HttpMethod method = network::detail::HttpMethod::kGet; + + 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_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 (data_source_config.with_reasons) { + url.append("?withReasons=true"); + } + + config::detail::builders::HttpPropertiesBuilder + builder(config.HttpProperties()); + + builder.Header("authorization", config.SdkKey()); + + return {url, method, builder.Build(), body}; +} + +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), + data_source_handler_( + DataSourceEventHandler(handler, logger, status_manager_)), + requester_(ioc), + timer_(ioc), + 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_ = polling_config.min_polling_interval; + } +} + +void PollingDataSource::DoPoll() { + 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(); + + if (etag_ && has_etag) { + if (etag_.value() == header_etag->second) { + // 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) { + 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()) { + 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 { + 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(); + }); +} + +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 == boost::asio::error::operation_aborted) { + // The timer was cancelled. Stop polling. + return; + } + if (ec) { + // Something unexpected happened. Log it and continue to try + // polling. + LD_LOG(logger_, LogLevel::kError) + << "Unexpected error in polling timer: " << ec.message(); + Close(); + } + DoPoll(); + }); +} + +void PollingDataSource::Start() { + DoPoll(); +} + +void PollingDataSource::Close() { + 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..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,49 +15,54 @@ 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) : logger_(logger), status_manager_(status_manager), data_source_handler_( - StreamingDataHandler(handler, logger, status_manager_)) { - auto uri_components = boost::urls::parse_uri(endpoints.StreamingBaseUrl()); + DataSourceEventHandler(handler, logger, status_manager_)) { + auto uri_components = + boost::urls::parse_uri(config.ServiceEndpoints().StreamingBaseUrl()); // TODO: Handle parsing error? + // TODO: Initial reconnect delay. boost::urls::url url = uri_components.value(); auto string_context = boost::json::serialize(boost::json::value_from(context)); + 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); + 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. }); @@ -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/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/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/config/detail/builders/data_source_builder.hpp b/libs/common/include/config/detail/builders/data_source_builder.hpp index 9d9d43073..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,15 +46,16 @@ 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_; }; /** * Contains methods for configuring the polling data source. */ +template class PollingBuilder { public: PollingBuilder(); @@ -69,23 +71,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()( - StreamingBuilder streaming) { + boost::variant, built::PollingConfig> operator()( + StreamingBuilder streaming) { return streaming.Build(); } - boost::variant operator()( - PollingBuilder polling) { + boost::variant, built::PollingConfig> operator()( + PollingBuilder polling) { return polling.Build(); } }; @@ -94,8 +97,8 @@ struct MethodVisitor { template <> class DataSourceBuilder { public: - using Streaming = StreamingBuilder; - using Polling = PollingBuilder; + using Streaming = StreamingBuilder; + using Polling = PollingBuilder; DataSourceBuilder(); @@ -167,8 +170,8 @@ class DataSourceBuilder { template <> class DataSourceBuilder { public: - using Streaming = StreamingBuilder; - using Polling = PollingBuilder; + using Streaming = StreamingBuilder; + using Polling = PollingBuilder; DataSourceBuilder(); 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/config/detail/built/data_source_config.hpp b/libs/common/include/config/detail/built/data_source_config.hpp index c6f396f13..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,11 +8,38 @@ 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; +}; + +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}; }; -struct PollingConfig { +template <> +struct PollingConfig { std::chrono::seconds poll_interval; }; @@ -21,7 +48,7 @@ struct DataSourceConfig; template <> struct DataSourceConfig { - boost::variant method; + boost::variant, PollingConfig> method; bool with_reasons; bool use_report; @@ -29,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 d9aa54fdb..141a245a8 100644 --- a/libs/common/include/config/detail/defaults.hpp +++ b/libs/common/include/config/detail/defaults.hpp @@ -20,15 +20,6 @@ struct Defaults { * @return */ static bool Offline() { return false; } - - 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 <> @@ -55,8 +46,17 @@ 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 { + // Default to 5 minutes; + return {std::chrono::seconds{5 * 60}}; } }; @@ -84,8 +84,16 @@ 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 { + return {std::chrono::seconds{30}}; } }; 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..17b1fc35c --- /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 diff --git a/libs/common/include/network/detail/http_requester.hpp b/libs/common/include/network/detail/http_requester.hpp index cb6fc0923..f962f76fc 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; @@ -87,6 +97,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_; @@ -94,7 +113,10 @@ class HttpRequest { std::string host_; std::string port_; std::string path_; + std::map params_; 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/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp index 750834dad..9aee748b7 100644 --- a/libs/common/src/config/data_source_builder.cpp +++ b/libs/common/src/config/data_source_builder.cpp @@ -2,28 +2,35 @@ 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_; } -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_; } @@ -43,19 +50,19 @@ DataSourceBuilder& DataSourceBuilder::UseReport( } DataSourceBuilder& DataSourceBuilder::Method( - StreamingBuilder builder) { + StreamingBuilder builder) { method_ = builder; return *this; } 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_}; } @@ -63,20 +70,26 @@ DataSourceBuilder::DataSourceBuilder() : with_reasons_(false), use_report_(false), method_(Streaming()) {} DataSourceBuilder& DataSourceBuilder::Method( - StreamingBuilder builder) { + StreamingBuilder builder) { method_ = builder; return *this; } 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; + +template class StreamingBuilder; +template class StreamingBuilder; + } // namespace launchdarkly::config::detail::builders 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_error_messages.cpp b/libs/common/src/network/http_error_messages.cpp new file mode 100644 index 000000000..2225ce921 --- /dev/null +++ b/libs/common/src/network/http_error_messages.cpp @@ -0,0 +1,20 @@ +#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"); + return error_message.str(); +} + +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 3bb1356b4..c3801d3c0 100644 --- a/libs/common/src/network/http_requester.cpp +++ b/libs/common/src/network/http_requester.cpp @@ -67,7 +67,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_ = "/"; } @@ -80,6 +81,17 @@ HttpRequest::HttpRequest(std::string const& url, } } +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 { return port_; } @@ -87,4 +99,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 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 773776dac..e92904112 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" @@ -20,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); } @@ -37,7 +40,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); } @@ -50,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); } @@ -63,6 +70,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); }