diff --git a/libs/common/include/config/detail/config.hpp b/libs/common/include/config/detail/config.hpp index 6f5af20ad..a9bb53a87 100644 --- a/libs/common/include/config/detail/config.hpp +++ b/libs/common/include/config/detail/config.hpp @@ -1,7 +1,9 @@ #pragma once +#include "config/detail/data_source_config.hpp" #include "config/detail/endpoints_builder.hpp" #include "config/detail/events_builder.hpp" +#include "config/detail/http_properties.hpp" namespace launchdarkly::config::detail { @@ -17,11 +19,15 @@ struct Config { detail::EndpointsBuilder service_endpoints_builder; std::optional application_tag; detail::EventsBuilder events_builder; + DataSourceConfig data_source_config; + detail::HttpProperties http_properties; Config(std::string sdk_key, bool offline, detail::EndpointsBuilder service_endpoints_builder, detail::EventsBuilder events_builder, - std::optional application_tag); + std::optional application_tag, + DataSourceConfig data_source_config, + detail::HttpProperties http_properties); }; } // namespace launchdarkly::config::detail diff --git a/libs/common/include/config/detail/config_builder.hpp b/libs/common/include/config/detail/config_builder.hpp index 2f6cf1091..888098e43 100644 --- a/libs/common/include/config/detail/config_builder.hpp +++ b/libs/common/include/config/detail/config_builder.hpp @@ -4,8 +4,10 @@ #include #include "config/detail/application_info.hpp" #include "config/detail/config.hpp" +#include "config/detail/data_source_builder.hpp" #include "config/detail/endpoints_builder.hpp" #include "config/detail/events_builder.hpp" +#include "config/detail/http_properties_builder.hpp" #include "logger.hpp" namespace launchdarkly::config::detail { @@ -21,6 +23,8 @@ class ConfigBuilder { using EndpointsBuilder = detail::EndpointsBuilder; using EventsBuilder = detail::EventsBuilder; using ConfigType = detail::Config; + using DataSourceBuilder = detail::DataSourceBuilder; + using HttpPropertiesBuilder = detail::HttpPropertiesBuilder; /** * A minimal configuration consists of a LaunchDarkly SDK Key. * @param sdk_key SDK Key. @@ -59,6 +63,22 @@ class ConfigBuilder { */ ConfigBuilder& events(EventsBuilder builder); + /** + * Sets the configuration of the component that receives feature flag data + * from LaunchDarkly. + * @param builder A DataSourceConfig builder. + * @return Reference to this builder. + */ + ConfigBuilder& data_source(detail::DataSourceBuilder builder); + + /** + * Sets the SDK's networking configuration, using an HttpPropertiesBuilder. + * The builder has methods for setting individual HTTP-related properties. + * @param builder A HttpPropertiesBuilder builder. + * @return Reference to this builder. + */ + ConfigBuilder& http_properties(detail::HttpPropertiesBuilder builder); + /** * Builds a Configuration, suitable for passing into an instance of Client. * @return @@ -71,6 +91,8 @@ class ConfigBuilder { std::optional service_endpoints_builder_; std::optional application_info_builder_; std::optional events_builder_; + std::optional data_source_builder_; + std::optional http_properties_builder_; }; } // namespace launchdarkly::config::detail diff --git a/libs/common/include/config/detail/data_source_builder.hpp b/libs/common/include/config/detail/data_source_builder.hpp new file mode 100644 index 000000000..5a5193b27 --- /dev/null +++ b/libs/common/include/config/detail/data_source_builder.hpp @@ -0,0 +1,212 @@ +#pragma once + +#include +#include +#include + +#include "config/detail/data_source_config.hpp" +#include "config/detail/defaults.hpp" +#include "config/detail/sdks.hpp" + +#include + +namespace launchdarkly::config::detail { + +/** + * Used to construct a DataSourceConfiguration for the specified SDK type. + * @tparam SDK ClientSDK or ServerSDK. + */ +template +class DataSourceBuilder; + +/** + * Builds a configuration for a streaming data source. + */ +class StreamingBuilder { + public: + StreamingBuilder(); + + /** + * Sets the initial reconnect delay for the streaming connection. + * + * The streaming service uses a backoff algorithm (with jitter) every time + * the connection needs to be reestablished.The delay for the first + * reconnection will start near this value, and then increase exponentially + * for any subsequent connection failures. + * + * @param initial_reconnect_delay The initial delay for a reconnection + * attempt. + * @return Reference to this builder. + */ + StreamingBuilder& initial_reconnect_delay( + std::chrono::milliseconds initial_reconnect_delay); + + /** + * Build the streaming config. Used internal to the SDK. + * @return The built config. + */ + [[nodiscard]] StreamingConfig build() const; + + private: + StreamingConfig config_; +}; + +/** + * Contains methods for configuring the polling data source. + */ +class PollingBuilder { + public: + PollingBuilder(); + + /** + * Sets the interval at which the SDK will poll for feature flag updates. + * @param poll_interval The polling interval. + * @return Reference to this builder. + */ + PollingBuilder& poll_interval(std::chrono::seconds poll_interval); + + /** + * Build the polling config. Used internal to the SDK. + * @return The built config. + */ + [[nodiscard]] PollingConfig build() const; + + private: + PollingConfig config_; +}; + +/** + * The method visitor is only needed inside this file + */ +namespace { +struct MethodVisitor { + boost::variant operator()( + StreamingBuilder streaming) { + return streaming.build(); + } + boost::variant operator()( + PollingBuilder polling) { + return polling.build(); + } +}; +} // namespace + +template <> +class DataSourceBuilder { + public: + using Streaming = StreamingBuilder; + using Polling = PollingBuilder; + + DataSourceBuilder(); + + /** + * Whether LaunchDarkly should provide additional information about how flag + * values were calculated. + * + * The additional information will then be available through the client's + * {TODO variation detail} method. Since this increases the size of network + * requests, such information is not sent unless you set this option to + * true. + * @param value True to enable reasons. + * @return Reference to this builder. + */ + DataSourceBuilder& with_reasons(bool value); + + /** + * Whether or not to use the REPORT verb to fetch flag settings. + * + * If this is true, flag settings will be fetched with a REPORT request + * including a JSON entity body with the context object. + * + * Otherwise (by default) a GET request will be issued with the context + * passed as a base64 URL-encoded path parameter. + * + * Do not use unless advised by LaunchDarkly. + * @param value True to enable using the REPORT verb. + * @return Reference to this builder. + */ + DataSourceBuilder& use_report(bool value); + + /** + * Set the streaming configuration for the builder. + * + * A data source may either be streaming or polling. Setting a streaming + * builder indicates the data source will use streaming. Setting a polling + * builder will indicate the use of polling. + * + * @param stream_builder The streaming builder. + * @return Reference to this builder. + */ + DataSourceBuilder& method(Streaming stream_builder); + + /** + * Set the polling configuration for the builder. + * + * A data source may either be streaming or polling. Setting a stream + * builder indicates the data source will use streaming. Setting a polling + * builder will indicate the use of polling. + * + * @param polling_builder The polling builder. + * @return Reference to this builder. + */ + DataSourceBuilder& method(Polling polling_builder); + + /** + * Build a data source config. This is used internal to the SDK. + * + * @return The built config. + */ + [[nodiscard]] DataSourceConfig build() const; + + private: + boost::variant method_; + bool with_reasons_; + bool use_report_; +}; + +template <> +class DataSourceBuilder { + public: + using Streaming = StreamingBuilder; + using Polling = PollingBuilder; + + DataSourceBuilder(); + + /** + * Set the streaming configuration for the builder. + * + * A data source may either be streaming or polling. Setting a streaming + * builder indicates the data source will use streaming. Setting a polling + * builder will indicate the use of polling. + * + * @param stream_builder The streaming builder. + * @return Reference to this builder. + */ + DataSourceBuilder& method(Streaming builder); + + /** + * Set the polling configuration for the builder. + * + * A data source may either be streaming or polling. Setting a stream + * builder indicates the data source will use streaming. Setting a polling + * builder will indicate the use of polling. + * + * @param polling_builder The polling builder. + * @return Reference to this builder. + */ + DataSourceBuilder& method(Polling builder); + + /** + * Build a data source config. This is used internal to the SDK. + * + * @return The built config. + */ + [[nodiscard]] DataSourceConfig build() const; + + private: + boost::variant method_; + bool with_reasons_; + bool use_report_; +}; + +} // namespace launchdarkly::config::detail diff --git a/libs/common/include/config/detail/data_source_config.hpp b/libs/common/include/config/detail/data_source_config.hpp new file mode 100644 index 000000000..4c2f11bfc --- /dev/null +++ b/libs/common/include/config/detail/data_source_config.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include +#include "config/detail/sdks.hpp" + +namespace launchdarkly::config::detail { + +struct StreamingConfig { + std::chrono::milliseconds initial_reconnect_delay; +}; + +struct PollingConfig { + std::chrono::seconds poll_interval; +}; + +template +struct DataSourceConfig; + +template <> +struct DataSourceConfig { + boost::variant method; + + bool with_reasons; + bool use_report; +}; + +template <> +struct DataSourceConfig { + boost::variant method; +}; + +} // namespace launchdarkly::config::detail diff --git a/libs/common/include/config/detail/defaults.hpp b/libs/common/include/config/detail/defaults.hpp index 8aae935c3..709fb225d 100644 --- a/libs/common/include/config/detail/defaults.hpp +++ b/libs/common/include/config/detail/defaults.hpp @@ -1,5 +1,6 @@ #pragma once +#include "config/detail/data_source_config.hpp" #include "config/detail/events.hpp" #include "config/detail/http_properties.hpp" #include "config/detail/sdks.hpp" @@ -19,6 +20,15 @@ struct Defaults { * @return */ static bool offline() { return false; } + + static StreamingConfig streaming_config() { + return {std::chrono::milliseconds{1000}}; + } + + static PollingConfig polling_config() { + // Default to 5 minutes; + return {std::chrono::seconds{5 * 60}}; + } }; template <> @@ -42,6 +52,10 @@ struct Defaults { sdk_name() + "/" + sdk_version(), std::map()}; } + + static DataSourceConfig data_source_config() { + return {Defaults::streaming_config(), false, false}; + } }; template <> @@ -65,6 +79,10 @@ struct Defaults { sdk_name() + "/" + sdk_version(), std::map()}; } + + static DataSourceConfig data_source_config() { + return {Defaults::streaming_config()}; + } }; } // namespace launchdarkly::config::detail diff --git a/libs/common/include/config/detail/http_properties_builder.hpp b/libs/common/include/config/detail/http_properties_builder.hpp index 917771772..4924e4e5b 100644 --- a/libs/common/include/config/detail/http_properties_builder.hpp +++ b/libs/common/include/config/detail/http_properties_builder.hpp @@ -27,7 +27,7 @@ class HttpPropertiesBuilder { HttpPropertiesBuilder& custom_headers( std::map base_headers); - HttpProperties build() const; + [[nodiscard]] HttpProperties build() const; private: std::chrono::milliseconds connect_timeout_{}; diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index 357e2d1e8..f385cd7ba 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -43,6 +43,7 @@ add_library(${LIBNAME} events/conn_pool.cpp events/summary_state.cpp config/http_properties.cpp + config/data_source_builder.cpp config/http_properties_builder.cpp) diff --git a/libs/common/src/config/config.cpp b/libs/common/src/config/config.cpp index 06b06e7d3..d57fea3db 100644 --- a/libs/common/src/config/config.cpp +++ b/libs/common/src/config/config.cpp @@ -1,3 +1,5 @@ +#include + #include "config/detail/config.hpp" #include "config/detail/sdks.hpp" @@ -7,12 +9,16 @@ Config::Config(std::string sdk_key, bool offline, detail::EndpointsBuilder service_endpoints_builder, detail::EventsBuilder events_builder, - std::optional application_tag) + std::optional application_tag, + DataSourceConfig data_source_config, + detail::HttpProperties http_properties) : sdk_key(std::move(sdk_key)), offline(offline), service_endpoints_builder(std::move(service_endpoints_builder)), events_builder(std::move(events_builder)), - application_tag(std::move(application_tag)) {} + application_tag(std::move(application_tag)), + data_source_config(std::move(data_source_config)), + http_properties(std::move(http_properties)) {} template class Config; template class Config; diff --git a/libs/common/src/config/config_builder.cpp b/libs/common/src/config/config_builder.cpp index 9fdfc9fe2..40df43ebb 100644 --- a/libs/common/src/config/config_builder.cpp +++ b/libs/common/src/config/config_builder.cpp @@ -35,6 +35,20 @@ ConfigBuilder& ConfigBuilder::offline(bool offline) { return *this; } +template +ConfigBuilder& ConfigBuilder::data_source( + detail::DataSourceBuilder builder) { + data_source_builder_ = builder; + return *this; +} + +template +ConfigBuilder& ConfigBuilder::http_properties( + detail::HttpPropertiesBuilder builder) { + http_properties_builder_ = builder; + return *this; +} + template typename ConfigBuilder::ConfigType ConfigBuilder::build( Logger& logger) const { @@ -46,7 +60,20 @@ typename ConfigBuilder::ConfigType ConfigBuilder::build( if (application_info_builder_) { app_tag = application_info_builder_->build(logger); } - return {key, offline, endpoints, events, app_tag}; + auto data_source_config = data_source_builder_ + ? data_source_builder_.value().build() + : Defaults::data_source_config(); + + auto http_properties = http_properties_builder_ + ? http_properties_builder_.value().build() + : Defaults::http_properties(); + return {std::move(key), + offline, + std::move(endpoints), + std::move(events), + std::move(app_tag), + std::move(data_source_config), + std::move(http_properties)}; } template class ConfigBuilder; diff --git a/libs/common/src/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp new file mode 100644 index 000000000..821f5de4a --- /dev/null +++ b/libs/common/src/config/data_source_builder.cpp @@ -0,0 +1,83 @@ +#include "config/detail/data_source_builder.hpp" + +namespace launchdarkly::config::detail { + +StreamingBuilder::StreamingBuilder() + : config_(Defaults::streaming_config()) {} + +StreamingBuilder& StreamingBuilder::initial_reconnect_delay( + std::chrono::milliseconds initial_reconnect_delay) { + config_.initial_reconnect_delay = initial_reconnect_delay; + return *this; +} + +StreamingConfig StreamingBuilder::build() const { + return config_; +} + +PollingBuilder::PollingBuilder() + : config_(Defaults::polling_config()) {} + +PollingBuilder& PollingBuilder::poll_interval( + std::chrono::seconds poll_interval) { + config_.poll_interval = poll_interval; + return *this; +} + +PollingConfig PollingBuilder::build() const { + return config_; +} + +DataSourceBuilder::DataSourceBuilder() + : with_reasons_(false), use_report_(false), method_(Streaming()) {} + +DataSourceBuilder& DataSourceBuilder::with_reasons( + bool value) { + with_reasons_ = value; + return *this; +} + +DataSourceBuilder& DataSourceBuilder::use_report( + bool value) { + use_report_ = value; + return *this; +} + +DataSourceBuilder& DataSourceBuilder::method( + StreamingBuilder builder) { + method_ = builder; + return *this; +} + +DataSourceBuilder& DataSourceBuilder::method( + PollingBuilder builder) { + method_ = builder; + return *this; +} + +DataSourceConfig DataSourceBuilder::build() const { + auto method = boost::apply_visitor(MethodVisitor(), method_); + return {method, with_reasons_, use_report_}; +} + +DataSourceBuilder::DataSourceBuilder() + : with_reasons_(false), use_report_(false), method_(Streaming()) {} + +DataSourceBuilder& DataSourceBuilder::method( + StreamingBuilder builder) { + method_ = builder; + return *this; +} + +DataSourceBuilder& DataSourceBuilder::method( + PollingBuilder builder) { + method_ = builder; + return *this; +} + +DataSourceConfig DataSourceBuilder::build() const { + auto method = boost::apply_visitor(MethodVisitor(), method_); + return {method}; +} + +} // namespace launchdarkly::config::detail diff --git a/libs/common/src/config/http_properties_builder.cpp b/libs/common/src/config/http_properties_builder.cpp index c5fa0c97c..9a8e401e1 100644 --- a/libs/common/src/config/http_properties_builder.cpp +++ b/libs/common/src/config/http_properties_builder.cpp @@ -1,5 +1,6 @@ #include +#include "config/detail/defaults.hpp" #include "config/detail/http_properties_builder.hpp" #include "config/detail/sdks.hpp" @@ -46,7 +47,9 @@ HttpProperties HttpPropertiesBuilder::build() const { std::map headers_with_wrapper(base_headers_); headers_with_wrapper["X-LaunchDarkly-Wrapper"] = wrapper_name_ + "/" + wrapper_version_; - return {connect_timeout_, read_timeout_, "", headers_with_wrapper}; + return {connect_timeout_, read_timeout_, + detail::Defaults::http_properties().user_agent(), + headers_with_wrapper}; } return {connect_timeout_, read_timeout_, "", base_headers_}; } diff --git a/libs/common/tests/config_builder_test.cpp b/libs/common/tests/config_builder_test.cpp index 6f2081e0a..25baa5243 100644 --- a/libs/common/tests/config_builder_test.cpp +++ b/libs/common/tests/config_builder_test.cpp @@ -59,3 +59,115 @@ TEST_F(ConfigBuilderTest, CustomBuilderReflectsChanges) { ASSERT_EQ(config.application_tag, "application-id/bar application-version/baz"); } + +TEST_F(ConfigBuilderTest, + DefaultConstruction_ClientConfig_UsesDefaulDataSourceConfig) { + using namespace launchdarkly::client; + ConfigBuilder builder("sdk-123"); + Config cfg = builder.build(logger); + + EXPECT_FALSE(cfg.data_source_config.with_reasons); + EXPECT_FALSE(cfg.data_source_config.use_report); + // Should be streaming with a 1 second initial retry; + EXPECT_EQ(std::chrono::milliseconds{1000}, + boost::get( + cfg.data_source_config.method) + .initial_reconnect_delay); +} + +TEST_F(ConfigBuilderTest, + DefaultConstruction_ServerConfig_UsesDefaulDataSourceConfig) { + using namespace launchdarkly::server; + ConfigBuilder builder("sdk-123"); + Config cfg = builder.build(logger); + + // Should be streaming with a 1 second initial retry; + EXPECT_EQ(std::chrono::milliseconds{1000}, + boost::get( + cfg.data_source_config.method) + .initial_reconnect_delay); +} + +TEST_F(ConfigBuilderTest, ServerConfig_CanSetDataSource) { + using namespace launchdarkly::server; + ConfigBuilder builder("sdk-123"); + + builder.data_source(ConfigBuilder::DataSourceBuilder().method( + ConfigBuilder::DataSourceBuilder::Streaming().initial_reconnect_delay( + std::chrono::milliseconds{5000}))); + + Config cfg = builder.build(logger); + + EXPECT_EQ(std::chrono::milliseconds{5000}, + boost::get( + cfg.data_source_config.method) + .initial_reconnect_delay); +} + +TEST_F(ConfigBuilderTest, ClientConfig_CanSetDataSource) { + using namespace launchdarkly::client; + ConfigBuilder builder("sdk-123"); + + builder.data_source( + ConfigBuilder::DataSourceBuilder() + .method( + ConfigBuilder::DataSourceBuilder::Streaming() + .initial_reconnect_delay(std::chrono::milliseconds{5000})) + .use_report(true) + .with_reasons(true)); + + Config cfg = builder.build(logger); + + EXPECT_TRUE(cfg.data_source_config.use_report); + EXPECT_TRUE(cfg.data_source_config.with_reasons); + EXPECT_EQ(std::chrono::milliseconds{5000}, + boost::get( + cfg.data_source_config.method) + .initial_reconnect_delay); +} + +TEST_F(ConfigBuilderTest, + DefaultConstruction_ClientConfig_UsesDefaultHttpProperties) { + using namespace launchdarkly::client; + ConfigBuilder builder("sdk-123"); + Config cfg = builder.build(logger); + + EXPECT_EQ("CppClient/TODO", cfg.http_properties.user_agent()); + EXPECT_EQ(10000, cfg.http_properties.read_timeout().count()); + EXPECT_EQ(10000, cfg.http_properties.connect_timeout().count()); + EXPECT_TRUE(cfg.http_properties.base_headers().empty()); +} + +TEST_F(ConfigBuilderTest, + DefaultConstruction_ServerConfig_UsesDefaultHttpProperties) { + using namespace launchdarkly::server; + ConfigBuilder builder("sdk-123"); + Config cfg = builder.build(logger); + + EXPECT_EQ("CppServer/TODO", cfg.http_properties.user_agent()); + EXPECT_EQ(10000, cfg.http_properties.read_timeout().count()); + EXPECT_EQ(2000, cfg.http_properties.connect_timeout().count()); + EXPECT_TRUE(cfg.http_properties.base_headers().empty()); +} + +TEST_F(ConfigBuilderTest, DefaultConstruction_CanSetHttpProperties) { + using namespace launchdarkly::client; + ConfigBuilder builder("sdk-123"); + builder.http_properties( + ConfigBuilder::HttpPropertiesBuilder() + .connect_timeout(std::chrono::milliseconds{1234}) + .read_timeout(std::chrono::milliseconds{123456}) + .wrapper_name("potato") + .wrapper_version("2.0-chip") + .custom_headers( + std::map{{"color", "green"}})); + + Config cfg = builder.build(logger); + + EXPECT_EQ("CppClient/TODO", cfg.http_properties.user_agent()); + EXPECT_EQ(123456, cfg.http_properties.read_timeout().count()); + EXPECT_EQ(1234, cfg.http_properties.connect_timeout().count()); + EXPECT_EQ("potato/2.0-chip", + cfg.http_properties.base_headers().at("X-LaunchDarkly-Wrapper")); + EXPECT_EQ("green", cfg.http_properties.base_headers().at("color")); +} diff --git a/libs/common/tests/data_source_builder_test.cpp b/libs/common/tests/data_source_builder_test.cpp new file mode 100644 index 000000000..ae60b6474 --- /dev/null +++ b/libs/common/tests/data_source_builder_test.cpp @@ -0,0 +1,66 @@ +#include "config/detail/data_source_builder.hpp" +#include +#include "value.hpp" + +#include + +using launchdarkly::config::detail::ClientSDK; +using launchdarkly::config::detail::DataSourceBuilder; +using launchdarkly::config::detail::PollingConfig; +using launchdarkly::config::detail::ServerSDK; +using launchdarkly::config::detail::StreamingConfig; + +using ClientDataSource = DataSourceBuilder; +using ServerDataSource = DataSourceBuilder; + +TEST(DataSourceBuilderTests, CanCreateStreamingClientConfig) { + auto client_config = + ClientDataSource() + .with_reasons(true) + .use_report(true) + .method(ClientDataSource::Streaming().initial_reconnect_delay( + std::chrono::milliseconds{1500})) + .build(); + + EXPECT_TRUE(client_config.use_report); + EXPECT_TRUE(client_config.with_reasons); + EXPECT_EQ(std::chrono::milliseconds{1500}, + boost::get(client_config.method) + .initial_reconnect_delay); +} + +TEST(DataSourceBuilderTests, CanCreatePollingClientConfig) { + auto client_config = ClientDataSource() + .with_reasons(false) + .use_report(false) + .method(ClientDataSource::Polling().poll_interval( + std::chrono::seconds{88000})) + .build(); + + EXPECT_FALSE(client_config.use_report); + EXPECT_FALSE(client_config.with_reasons); + EXPECT_EQ(std::chrono::seconds{88000}, + boost::get(client_config.method).poll_interval); +} + +TEST(DataSourceBuilderTests, CanCreateStreamingServerConfig) { + auto server_config = + ServerDataSource() + .method(ServerDataSource::Streaming().initial_reconnect_delay( + std::chrono::milliseconds{1500})) + .build(); + + EXPECT_EQ(std::chrono::milliseconds{1500}, + boost::get(server_config.method) + .initial_reconnect_delay); +} + +TEST(DataSourceBuilderTests, CanCreatePollingServerConfig) { + auto server_config = ServerDataSource() + .method(ServerDataSource::Polling().poll_interval( + std::chrono::seconds{30000})) + .build(); + + EXPECT_EQ(std::chrono::seconds{30000}, + boost::get(server_config.method).poll_interval); +}