From 0e1936f428f19fb44822f75c082b3610d3d00e6e Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Wed, 4 Mar 2026 16:33:54 -0500 Subject: [PATCH 1/5] fix: Move fdv2 data source builders into public API --- lib/ldclient-rb/config.rb | 8 +- lib/ldclient-rb/data_system.rb | 349 +++++++++++++++++- .../data_system/data_source_builder_common.rb | 134 +++---- lib/ldclient-rb/impl/data_system/polling.rb | 124 +------ lib/ldclient-rb/impl/data_system/streaming.rb | 46 +-- 5 files changed, 434 insertions(+), 227 deletions(-) diff --git a/lib/ldclient-rb/config.rb b/lib/ldclient-rb/config.rb index aa4d1d67..6d123938 100644 --- a/lib/ldclient-rb/config.rb +++ b/lib/ldclient-rb/config.rb @@ -468,7 +468,7 @@ def self.default_capacity # @return [String] "https://sdk.launchdarkly.com" # def self.default_base_uri - Impl::DataSystem::PollingDataSourceBuilder::DEFAULT_BASE_URI + Impl::DataSystem::DEFAULT_POLLING_BASE_URI end # @@ -476,7 +476,7 @@ def self.default_base_uri # @return [String] "https://stream.launchdarkly.com" # def self.default_stream_uri - Impl::DataSystem::StreamingDataSourceBuilder::DEFAULT_BASE_URI + Impl::DataSystem::DEFAULT_STREAMING_BASE_URI end # @@ -516,7 +516,7 @@ def self.default_read_timeout # @return [Float] 1 # def self.default_initial_reconnect_delay - Impl::DataSystem::StreamingDataSourceBuilder::DEFAULT_INITIAL_RECONNECT_DELAY + Impl::DataSystem::DEFAULT_INITIAL_RECONNECT_DELAY end # @@ -578,7 +578,7 @@ def self.default_offline # @return [Float] 30 # def self.default_poll_interval - Impl::DataSystem::PollingDataSourceBuilder::DEFAULT_POLL_INTERVAL + Impl::DataSystem::DEFAULT_POLL_INTERVAL end # diff --git a/lib/ldclient-rb/data_system.rb b/lib/ldclient-rb/data_system.rb index faed96a8..8e83e51d 100644 --- a/lib/ldclient-rb/data_system.rb +++ b/lib/ldclient-rb/data_system.rb @@ -9,12 +9,95 @@ module LaunchDarkly # # Configuration for LaunchDarkly's data acquisition strategy. # - # This module provides factory methods for creating data system configurations. + # This module provides factory methods for creating data system configurations, + # as well as builder classes for constructing individual data sources (polling + # and streaming). + # + # == Quick Start + # + # For most users, the predefined strategies are sufficient: + # + # # Use the default strategy (recommended) + # config = LaunchDarkly::Config.new( + # data_system: LaunchDarkly::DataSystem.default + # ) + # + # # Use streaming only + # config = LaunchDarkly::Config.new( + # data_system: LaunchDarkly::DataSystem.streaming + # ) + # + # # Use polling only + # config = LaunchDarkly::Config.new( + # data_system: LaunchDarkly::DataSystem.polling + # ) + # + # == Custom Configurations + # + # For advanced use cases, you can build custom configurations using the + # data source builders: + # + # polling = LaunchDarkly::DataSystem.polling_ds_builder + # .poll_interval(60) + # .base_uri("https://custom-polling.example.com") + # + # streaming = LaunchDarkly::DataSystem.streaming_ds_builder + # .initial_reconnect_delay(2) + # .base_uri("https://custom-streaming.example.com") + # + # data_system = LaunchDarkly::DataSystem.custom + # .initializers([polling]) + # .synchronizers([streaming, polling]) + # + # config = LaunchDarkly::Config.new(data_system: data_system) # module DataSystem + # + # Interface for custom polling requesters. + # + # A Requester is responsible for fetching data from a data source. The SDK + # ships with built-in HTTP requesters for both FDv2 and FDv1 polling endpoints, + # but you can implement this interface to provide custom data fetching logic + # (e.g., reading from a file, a database, or a custom API). + # + # == Implementing a Custom Requester + # + # To create a custom requester, include this module and implement the {#fetch} + # method: + # + # class MyCustomRequester + # include LaunchDarkly::DataSystem::Requester + # + # def fetch(selector) + # # Fetch data and return a Result containing [ChangeSet, headers] + # # ... + # LaunchDarkly::Result.success([change_set, {}]) + # end + # + # def stop + # # Clean up resources + # end + # end + # + # polling = LaunchDarkly::DataSystem.polling_ds_builder + # .requester(MyCustomRequester.new) + # + # @see PollingDataSourceBuilder#requester + # + Requester = LaunchDarkly::Impl::DataSystem::Requester + # # Builder for the data system configuration. # + # This builder configures the overall data acquisition strategy for the SDK, + # including which data sources to use for initialization and synchronization, + # and how to interact with a persistent data store. + # + # @see DataSystem.default + # @see DataSystem.streaming + # @see DataSystem.polling + # @see DataSystem.custom + # class ConfigBuilder def initialize @initializers = nil @@ -27,6 +110,9 @@ def initialize # # Sets the initializers for the data system. # + # Initializers are used to fetch an initial set of data when the SDK starts. + # They are tried in order; if the first one fails, the next is tried, and so on. + # # @param initializers [Array<#build(String, Config)>] # Array of builders that respond to build(sdk_key, config) and return an Initializer # @return [ConfigBuilder] self for chaining @@ -39,6 +125,10 @@ def initializers(initializers) # # Sets the synchronizers for the data system. # + # Synchronizers keep data up-to-date after initialization. Like initializers, + # they are tried in order. If the primary synchronizer fails, the next one + # takes over. + # # @param synchronizers [Array<#build(String, Config)>] # Array of builders that respond to build(sdk_key, config) and return a Synchronizer # @return [ConfigBuilder] self for chaining @@ -52,6 +142,9 @@ def synchronizers(synchronizers) # Configures the SDK with a fallback synchronizer that is compatible with # the Flag Delivery v1 API. # + # This fallback is used when the server signals that the environment should + # revert to FDv1 protocol. Most users will not need to set this directly. + # # @param fallback [#build(String, Config)] Builder that responds to build(sdk_key, config) and returns the fallback Synchronizer # @return [ConfigBuilder] self for chaining # @@ -64,7 +157,8 @@ def fdv1_compatible_synchronizer(fallback) # Sets the data store configuration for the data system. # # @param data_store [LaunchDarkly::Interfaces::FeatureStore] The data store - # @param store_mode [Symbol] The store mode + # @param store_mode [Symbol] The store mode (use constants from + # {LaunchDarkly::Interfaces::DataSystem::DataStoreMode}) # @return [ConfigBuilder] self for chaining # def data_store(data_store, store_mode) @@ -89,15 +183,243 @@ def build end end + # + # Builder for a polling data source that communicates with LaunchDarkly's + # FDv2 polling endpoint. + # + # This builder can be used with {ConfigBuilder#initializers} or + # {ConfigBuilder#synchronizers} to create custom data system configurations. + # + # The polling data source periodically fetches data from LaunchDarkly. It + # supports conditional requests via ETags, so subsequent polls after the + # initial request only transfer data if changes have occurred. + # + # == Example + # + # polling = LaunchDarkly::DataSystem.polling_ds_builder + # .poll_interval(60) + # .base_uri("https://custom-endpoint.example.com") + # + # data_system = LaunchDarkly::DataSystem.custom + # .synchronizers([polling]) + # + # @see DataSystem.polling_ds_builder + # + class PollingDataSourceBuilder + include LaunchDarkly::DataSystem::DataSourceBuilderCommon + + # @return [String] The default base URI for polling requests + DEFAULT_BASE_URI = LaunchDarkly::Impl::DataSystem::DEFAULT_POLLING_BASE_URI + + # @return [Float] The default polling interval in seconds + DEFAULT_POLL_INTERVAL = LaunchDarkly::Impl::DataSystem::DEFAULT_POLL_INTERVAL + + def initialize + @requester = nil + end + + # + # Sets the polling interval in seconds. + # + # This controls how frequently the SDK polls LaunchDarkly for updates. + # Lower values mean more frequent updates but higher network traffic. + # The default is {DEFAULT_POLL_INTERVAL} seconds. + # + # @param secs [Float] Polling interval in seconds + # @return [PollingDataSourceBuilder] self for chaining + # + def poll_interval(secs) + @poll_interval = secs + self + end + + # + # Sets a custom {Requester} for this polling data source. + # + # By default, the builder uses an HTTP requester that communicates with + # LaunchDarkly's FDv2 polling endpoint. Use this method to provide a + # custom requester implementation for testing or non-standard environments. + # + # @param requester [Requester] A custom requester that implements the + # {Requester} interface + # @return [PollingDataSourceBuilder] self for chaining + # + # @see Requester + # + def requester(requester) + @requester = requester + self + end + + # + # Builds the polling data source with the configured parameters. + # + # This method is called internally by the SDK. You do not need to call it + # directly; instead, pass the builder to {ConfigBuilder#initializers} or + # {ConfigBuilder#synchronizers}. + # + # @param sdk_key [String] The SDK key + # @param config [LaunchDarkly::Config] The SDK configuration + # @return [LaunchDarkly::Impl::DataSystem::PollingDataSource] + # + def build(sdk_key, config) + http_opts = build_http_config + requester = @requester || LaunchDarkly::Impl::DataSystem::HTTPPollingRequester.new(sdk_key, http_opts, config) + LaunchDarkly::Impl::DataSystem::PollingDataSource.new(@poll_interval || DEFAULT_POLL_INTERVAL, requester, config.logger) + end + end + + # + # Builder for a polling data source that communicates with LaunchDarkly's + # FDv1 (Flag Delivery v1) polling endpoint. + # + # This builder is typically used with {ConfigBuilder#fdv1_compatible_synchronizer} + # to provide a fallback when the server signals that the environment should + # revert to the FDv1 protocol. + # + # Most users will not need to interact with this builder directly, as the + # predefined strategies ({DataSystem.default}, {DataSystem.streaming}, + # {DataSystem.polling}) already configure an appropriate FDv1 fallback. + # + # @see DataSystem.fdv1_fallback_ds_builder + # + class FDv1PollingDataSourceBuilder + include LaunchDarkly::DataSystem::DataSourceBuilderCommon + + # @return [String] The default base URI for FDv1 polling requests + DEFAULT_BASE_URI = LaunchDarkly::Impl::DataSystem::DEFAULT_POLLING_BASE_URI + + # @return [Float] The default polling interval in seconds + DEFAULT_POLL_INTERVAL = LaunchDarkly::Impl::DataSystem::DEFAULT_POLL_INTERVAL + + def initialize + @requester = nil + end + + # + # Sets the polling interval in seconds. + # + # This controls how frequently the SDK polls LaunchDarkly for updates. + # The default is {DEFAULT_POLL_INTERVAL} seconds. + # + # @param secs [Float] Polling interval in seconds + # @return [FDv1PollingDataSourceBuilder] self for chaining + # + def poll_interval(secs) + @poll_interval = secs + self + end + + # + # Sets a custom {Requester} for this polling data source. + # + # By default, the builder uses an HTTP requester that communicates with + # LaunchDarkly's FDv1 polling endpoint. Use this method to provide a + # custom requester implementation. + # + # @param requester [Requester] A custom requester that implements the + # {Requester} interface + # @return [FDv1PollingDataSourceBuilder] self for chaining + # + # @see Requester + # + def requester(requester) + @requester = requester + self + end + + # + # Builds the FDv1 polling data source with the configured parameters. + # + # This method is called internally by the SDK. You do not need to call it + # directly; instead, pass the builder to {ConfigBuilder#fdv1_compatible_synchronizer}. + # + # @param sdk_key [String] The SDK key + # @param config [LaunchDarkly::Config] The SDK configuration + # @return [LaunchDarkly::Impl::DataSystem::PollingDataSource] + # + def build(sdk_key, config) + http_opts = build_http_config + requester = @requester || LaunchDarkly::Impl::DataSystem::HTTPFDv1PollingRequester.new(sdk_key, http_opts, config) + LaunchDarkly::Impl::DataSystem::PollingDataSource.new(@poll_interval || DEFAULT_POLL_INTERVAL, requester, config.logger) + end + end + + # + # Builder for a streaming data source that uses Server-Sent Events (SSE) + # to receive real-time updates from LaunchDarkly's Flag Delivery services. + # + # This builder can be used with {ConfigBuilder#synchronizers} to create + # custom data system configurations. Streaming provides the lowest latency + # for flag updates compared to polling. + # + # == Example + # + # streaming = LaunchDarkly::DataSystem.streaming_ds_builder + # .initial_reconnect_delay(2) + # .base_uri("https://custom-stream.example.com") + # + # data_system = LaunchDarkly::DataSystem.custom + # .synchronizers([streaming]) + # + # @see DataSystem.streaming_ds_builder + # + class StreamingDataSourceBuilder + include LaunchDarkly::DataSystem::DataSourceBuilderCommon + + # @return [String] The default base URI for streaming connections + DEFAULT_BASE_URI = LaunchDarkly::Impl::DataSystem::DEFAULT_STREAMING_BASE_URI + + # @return [Float] The default initial reconnect delay in seconds + DEFAULT_INITIAL_RECONNECT_DELAY = LaunchDarkly::Impl::DataSystem::DEFAULT_INITIAL_RECONNECT_DELAY + + def initialize + # No initialization needed - defaults applied in build via nil-check + end + + # + # Sets the initial delay before reconnecting after a stream connection error. + # + # The SDK uses an exponential backoff strategy starting from this delay. + # The default is {DEFAULT_INITIAL_RECONNECT_DELAY} second. + # + # @param delay [Float] Delay in seconds + # @return [StreamingDataSourceBuilder] self for chaining + # + def initial_reconnect_delay(delay) + @initial_reconnect_delay = delay + self + end + + # + # Builds the streaming data source with the configured parameters. + # + # This method is called internally by the SDK. You do not need to call it + # directly; instead, pass the builder to {ConfigBuilder#synchronizers}. + # + # @param sdk_key [String] The SDK key + # @param config [LaunchDarkly::Config] The SDK configuration + # @return [LaunchDarkly::Impl::DataSystem::StreamingDataSource] + # + def build(sdk_key, config) + http_opts = build_http_config + LaunchDarkly::Impl::DataSystem::StreamingDataSource.new( + sdk_key, http_opts, + @initial_reconnect_delay || DEFAULT_INITIAL_RECONNECT_DELAY, + config + ) + end + end + # # Returns a builder for creating a polling data source. # This is a building block that can be used with {ConfigBuilder#initializers} # or {ConfigBuilder#synchronizers} to create custom data system configurations. # - # @return [LaunchDarkly::Impl::DataSystem::PollingDataSourceBuilder] + # @return [PollingDataSourceBuilder] # def self.polling_ds_builder - LaunchDarkly::Impl::DataSystem::PollingDataSourceBuilder.new + PollingDataSourceBuilder.new end # @@ -105,10 +427,10 @@ def self.polling_ds_builder # This is a building block that can be used with {ConfigBuilder#fdv1_compatible_synchronizer} # to provide FDv1 compatibility in custom data system configurations. # - # @return [LaunchDarkly::Impl::DataSystem::FDv1PollingDataSourceBuilder] + # @return [FDv1PollingDataSourceBuilder] # def self.fdv1_fallback_ds_builder - LaunchDarkly::Impl::DataSystem::FDv1PollingDataSourceBuilder.new + FDv1PollingDataSourceBuilder.new end # @@ -116,10 +438,10 @@ def self.fdv1_fallback_ds_builder # This is a building block that can be used with {ConfigBuilder#synchronizers} # to create custom data system configurations. # - # @return [LaunchDarkly::Impl::DataSystem::StreamingDataSourceBuilder] + # @return [StreamingDataSourceBuilder] # def self.streaming_ds_builder - LaunchDarkly::Impl::DataSystem::StreamingDataSourceBuilder.new + StreamingDataSourceBuilder.new end # @@ -225,3 +547,14 @@ def self.persistent_store(store) end end +# Backward-compatible aliases so that existing code referencing the Impl namespace +# (including internal specs) continues to work. +module LaunchDarkly + module Impl + module DataSystem + PollingDataSourceBuilder = LaunchDarkly::DataSystem::PollingDataSourceBuilder + FDv1PollingDataSourceBuilder = LaunchDarkly::DataSystem::FDv1PollingDataSourceBuilder + StreamingDataSourceBuilder = LaunchDarkly::DataSystem::StreamingDataSourceBuilder + end + end +end diff --git a/lib/ldclient-rb/impl/data_system/data_source_builder_common.rb b/lib/ldclient-rb/impl/data_system/data_source_builder_common.rb index c2eede08..64791651 100644 --- a/lib/ldclient-rb/impl/data_system/data_source_builder_common.rb +++ b/lib/ldclient-rb/impl/data_system/data_source_builder_common.rb @@ -3,75 +3,81 @@ require "ldclient-rb/impl/data_system/http_config_options" module LaunchDarkly - module Impl - module DataSystem - # - # DataSourceBuilderCommon is a mixin that provides common HTTP configuration - # setters for data source builders (polling and streaming). - # - # Each builder that includes this module must define a DEFAULT_BASE_URI constant. - # - module DataSourceBuilderCommon - # - # Sets the base URI for HTTP requests. - # - # @param uri [String] - # @return [self] - # - def base_uri(uri) - @base_uri = uri - self - end + module DataSystem + # + # Common HTTP configuration methods shared by all data source builders. + # + # This module is included by {PollingDataSourceBuilder}, + # {FDv1PollingDataSourceBuilder}, and {StreamingDataSourceBuilder} to provide + # a consistent set of HTTP connection settings. + # + # Each builder that includes this module must define a +DEFAULT_BASE_URI+ constant + # which is used as the fallback when {#base_uri} has not been called. + # + module DataSourceBuilderCommon + # + # Sets the base URI for HTTP requests. + # + # Use this to point the SDK at a Relay Proxy instance or any other URI + # that implements the corresponding LaunchDarkly API. + # + # @param uri [String] The base URI (e.g. "https://relay.example.com") + # @return [self] the builder, for chaining + # + def base_uri(uri) + @base_uri = uri + self + end - # - # Sets a custom socket factory for HTTP connections. - # - # @param factory [Object] - # @return [self] - # - def socket_factory(factory) - @socket_factory = factory - self - end + # + # Sets a custom socket factory for HTTP connections. + # + # @param factory [#open] A socket factory that responds to +open+ + # @return [self] the builder, for chaining + # + def socket_factory(factory) + @socket_factory = factory + self + end - # - # Sets the read timeout for HTTP connections. - # - # @param timeout [Float] Timeout in seconds - # @return [self] - # - def read_timeout(timeout) - @read_timeout = timeout - self - end + # + # Sets the read timeout for HTTP connections. + # + # @param timeout [Float] Timeout in seconds + # @return [self] the builder, for chaining + # + def read_timeout(timeout) + @read_timeout = timeout + self + end - # - # Sets the connect timeout for HTTP connections. - # - # @param timeout [Float] Timeout in seconds - # @return [self] - # - def connect_timeout(timeout) - @connect_timeout = timeout - self - end + # + # Sets the connect timeout for HTTP connections. + # + # @param timeout [Float] Timeout in seconds + # @return [self] the builder, for chaining + # + def connect_timeout(timeout) + @connect_timeout = timeout + self + end - # - # Builds an HttpConfigOptions instance from the current builder settings. - # Uses self.class::DEFAULT_BASE_URI if base_uri was not explicitly set. - # Read/connect timeouts default to HttpConfigOptions defaults if not set. - # - # @return [HttpConfigOptions] - # - private def build_http_config - HttpConfigOptions.new( - base_uri: (@base_uri || self.class::DEFAULT_BASE_URI).chomp("/"), - socket_factory: @socket_factory, - read_timeout: @read_timeout, - connect_timeout: @connect_timeout - ) - end + # + # Builds an HttpConfigOptions instance from the current builder settings. + # Uses +self.class::DEFAULT_BASE_URI+ if {#base_uri} was not explicitly set. + # Read/connect timeouts default to HttpConfigOptions defaults if not set. + # + # @return [LaunchDarkly::Impl::DataSystem::HttpConfigOptions] + # + private def build_http_config + LaunchDarkly::Impl::DataSystem::HttpConfigOptions.new( + base_uri: (@base_uri || self.class::DEFAULT_BASE_URI).chomp("/"), + socket_factory: @socket_factory, + read_timeout: @read_timeout, + connect_timeout: @connect_timeout + ) end end end + end diff --git a/lib/ldclient-rb/impl/data_system/polling.rb b/lib/ldclient-rb/impl/data_system/polling.rb index 7942571f..22f5cc5c 100644 --- a/lib/ldclient-rb/impl/data_system/polling.rb +++ b/lib/ldclient-rb/impl/data_system/polling.rb @@ -21,27 +21,26 @@ module DataSystem LD_ENVID_HEADER = "X-LD-EnvID" LD_FD_FALLBACK_HEADER = "X-LD-FD-Fallback" + # Default base URI for polling requests. + DEFAULT_POLLING_BASE_URI = "https://sdk.launchdarkly.com" + + # Default polling interval in seconds. + DEFAULT_POLL_INTERVAL = 30 + # - # Requester protocol for polling data source + # @deprecated Use {LaunchDarkly::DataSystem::Requester} instead. This module + # remains here for backward compatibility and for use by internal classes + # that are loaded before the public module. + # + # @see LaunchDarkly::DataSystem::Requester # module Requester - # - # Fetches the data for the given selector. - # Returns a Result containing a tuple of [ChangeSet, headers], - # or an error if the data could not be retrieved. - # - # @param selector [LaunchDarkly::Interfaces::DataSystem::Selector, nil] - # @return [Result] - # + # @see LaunchDarkly::DataSystem::Requester#fetch def fetch(selector) raise NotImplementedError end - # - # Closes any persistent connections and releases resources. - # This method should be called when the requester is no longer needed. - # Implementations should handle being called multiple times gracefully. - # + # @see LaunchDarkly::DataSystem::Requester#stop def stop # Optional - implementations may override if they need cleanup end @@ -526,103 +525,6 @@ def self.fdv1_polling_payload_to_changeset(data) LaunchDarkly::Result.success(builder.finish(selector)) end - # - # Builder for a PollingDataSource. - # - class PollingDataSourceBuilder - include DataSourceBuilderCommon - - DEFAULT_BASE_URI = "https://sdk.launchdarkly.com" - DEFAULT_POLL_INTERVAL = 30 - - def initialize - @requester = nil - end - - # - # Sets the polling interval in seconds. - # - # @param secs [Float] Polling interval in seconds - # @return [PollingDataSourceBuilder] - # - def poll_interval(secs) - @poll_interval = secs - self - end - - # - # Sets a custom Requester for the PollingDataSource. - # - # @param requester [Requester] - # @return [PollingDataSourceBuilder] - # - def requester(requester) - @requester = requester - self - end - - # - # Builds the PollingDataSource with the configured parameters. - # - # @param sdk_key [String] - # @param config [LaunchDarkly::Config] - # @return [PollingDataSource] - # - def build(sdk_key, config) - http_opts = build_http_config - requester = @requester || HTTPPollingRequester.new(sdk_key, http_opts, config) - PollingDataSource.new(@poll_interval || DEFAULT_POLL_INTERVAL, requester, config.logger) - end - end - - # - # Builder for an FDv1 PollingDataSource. - # - class FDv1PollingDataSourceBuilder - include DataSourceBuilderCommon - - DEFAULT_BASE_URI = "https://sdk.launchdarkly.com" - DEFAULT_POLL_INTERVAL = 30 - - def initialize - @requester = nil - end - - # - # Sets the polling interval in seconds. - # - # @param secs [Float] Polling interval in seconds - # @return [FDv1PollingDataSourceBuilder] - # - def poll_interval(secs) - @poll_interval = secs - self - end - - # - # Sets a custom Requester for the PollingDataSource. - # - # @param requester [Requester] - # @return [FDv1PollingDataSourceBuilder] - # - def requester(requester) - @requester = requester - self - end - - # - # Builds the PollingDataSource with the configured parameters. - # - # @param sdk_key [String] - # @param config [LaunchDarkly::Config] - # @return [PollingDataSource] - # - def build(sdk_key, config) - http_opts = build_http_config - requester = @requester || HTTPFDv1PollingRequester.new(sdk_key, http_opts, config) - PollingDataSource.new(@poll_interval || DEFAULT_POLL_INTERVAL, requester, config.logger) - end - end end end end diff --git a/lib/ldclient-rb/impl/data_system/streaming.rb b/lib/ldclient-rb/impl/data_system/streaming.rb index 2947aa63..f4debb90 100644 --- a/lib/ldclient-rb/impl/data_system/streaming.rb +++ b/lib/ldclient-rb/impl/data_system/streaming.rb @@ -21,6 +21,12 @@ module DataSystem # The heartbeats sent as comments on the stream will keep this from triggering. STREAM_READ_TIMEOUT = 5 * 60 + # Default base URI for streaming connections. + DEFAULT_STREAMING_BASE_URI = "https://stream.launchdarkly.com" + + # Default initial delay before reconnecting after an error, in seconds. + DEFAULT_INITIAL_RECONNECT_DELAY = 1 + # # StreamingDataSource is a Synchronizer that uses Server-Sent Events (SSE) # to receive real-time updates from LaunchDarkly's Flag Delivery services. @@ -356,46 +362,6 @@ def stop end end - # - # Builder for a StreamingDataSource. - # - class StreamingDataSourceBuilder - include DataSourceBuilderCommon - - DEFAULT_BASE_URI = "https://stream.launchdarkly.com" - DEFAULT_INITIAL_RECONNECT_DELAY = 1 - - def initialize - # No initialization needed - defaults applied in build via nil-check - end - - # - # Sets the initial delay before reconnecting after an error. - # - # @param delay [Float] Delay in seconds - # @return [StreamingDataSourceBuilder] - # - def initial_reconnect_delay(delay) - @initial_reconnect_delay = delay - self - end - - # - # Builds the StreamingDataSource with the configured parameters. - # - # @param sdk_key [String] - # @param config [LaunchDarkly::Config] - # @return [StreamingDataSource] - # - def build(sdk_key, config) - http_opts = build_http_config - StreamingDataSource.new( - sdk_key, http_opts, - @initial_reconnect_delay || DEFAULT_INITIAL_RECONNECT_DELAY, - config - ) - end - end end end end From 260cdd64cabc8dccced1630522bc67d47fc5f142 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Thu, 5 Mar 2026 09:18:50 -0500 Subject: [PATCH 2/5] Remove silly backwards compatible block --- lib/ldclient-rb/data_system.rb | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/ldclient-rb/data_system.rb b/lib/ldclient-rb/data_system.rb index 8e83e51d..961dbb9d 100644 --- a/lib/ldclient-rb/data_system.rb +++ b/lib/ldclient-rb/data_system.rb @@ -546,15 +546,3 @@ def self.persistent_store(store) end end end - -# Backward-compatible aliases so that existing code referencing the Impl namespace -# (including internal specs) continues to work. -module LaunchDarkly - module Impl - module DataSystem - PollingDataSourceBuilder = LaunchDarkly::DataSystem::PollingDataSourceBuilder - FDv1PollingDataSourceBuilder = LaunchDarkly::DataSystem::FDv1PollingDataSourceBuilder - StreamingDataSourceBuilder = LaunchDarkly::DataSystem::StreamingDataSourceBuilder - end - end -end From 545c272337f311b4100438fd2897ba793844f8ff Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Thu, 5 Mar 2026 09:21:18 -0500 Subject: [PATCH 3/5] fix specs --- spec/impl/data_system/streaming_headers_spec.rb | 2 +- spec/impl/data_system/streaming_synchronizer_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/impl/data_system/streaming_headers_spec.rb b/spec/impl/data_system/streaming_headers_spec.rb index e646e7cf..2b059b9b 100644 --- a/spec/impl/data_system/streaming_headers_spec.rb +++ b/spec/impl/data_system/streaming_headers_spec.rb @@ -22,7 +22,7 @@ module DataSystem ) end - let(:synchronizer) { StreamingDataSourceBuilder.new.build(sdk_key, config) } + let(:synchronizer) { LaunchDarkly::DataSystem::StreamingDataSourceBuilder.new.build(sdk_key, config) } describe "on_error callback" do it "triggers FDv1 fallback when X-LD-FD-FALLBACK header is true" do diff --git a/spec/impl/data_system/streaming_synchronizer_spec.rb b/spec/impl/data_system/streaming_synchronizer_spec.rb index 04b20399..11351e76 100644 --- a/spec/impl/data_system/streaming_synchronizer_spec.rb +++ b/spec/impl/data_system/streaming_synchronizer_spec.rb @@ -34,7 +34,7 @@ def initialize(type, data = nil) end describe '#process_message' do - let(:synchronizer) { StreamingDataSourceBuilder.new.build(sdk_key, config) } + let(:synchronizer) { LaunchDarkly::DataSystem::StreamingDataSourceBuilder.new.build(sdk_key, config) } let(:change_set_builder) { LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new } let(:envid) { nil } @@ -312,7 +312,7 @@ def initialize(type, data = nil) end describe 'diagnostic event recording' do - let(:synchronizer) { StreamingDataSourceBuilder.new.build(sdk_key, config) } + let(:synchronizer) { LaunchDarkly::DataSystem::StreamingDataSourceBuilder.new.build(sdk_key, config) } it "logs successful connection when diagnostic_accumulator is provided" do diagnostic_accumulator = double("DiagnosticAccumulator") From 21188a3f513b07d16e2e8e97c42f233833d326c8 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Thu, 5 Mar 2026 09:37:42 -0500 Subject: [PATCH 4/5] move common into data system directory --- .../{impl => }/data_system/data_source_builder_common.rb | 1 - lib/ldclient-rb/impl/data_system/polling.rb | 2 +- lib/ldclient-rb/impl/data_system/streaming.rb | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) rename lib/ldclient-rb/{impl => }/data_system/data_source_builder_common.rb (99%) diff --git a/lib/ldclient-rb/impl/data_system/data_source_builder_common.rb b/lib/ldclient-rb/data_system/data_source_builder_common.rb similarity index 99% rename from lib/ldclient-rb/impl/data_system/data_source_builder_common.rb rename to lib/ldclient-rb/data_system/data_source_builder_common.rb index 64791651..c264fc61 100644 --- a/lib/ldclient-rb/impl/data_system/data_source_builder_common.rb +++ b/lib/ldclient-rb/data_system/data_source_builder_common.rb @@ -79,5 +79,4 @@ def connect_timeout(timeout) end end end - end diff --git a/lib/ldclient-rb/impl/data_system/polling.rb b/lib/ldclient-rb/impl/data_system/polling.rb index 22f5cc5c..9f54e3ee 100644 --- a/lib/ldclient-rb/impl/data_system/polling.rb +++ b/lib/ldclient-rb/impl/data_system/polling.rb @@ -4,7 +4,7 @@ require "ldclient-rb/interfaces/data_system" require "ldclient-rb/impl/data_system" require "ldclient-rb/impl/data_system/protocolv2" -require "ldclient-rb/impl/data_system/data_source_builder_common" +require "ldclient-rb/data_system/data_source_builder_common" require "ldclient-rb/impl/data_source/requestor" require "ldclient-rb/impl/util" require "concurrent" diff --git a/lib/ldclient-rb/impl/data_system/streaming.rb b/lib/ldclient-rb/impl/data_system/streaming.rb index f4debb90..5eb8127d 100644 --- a/lib/ldclient-rb/impl/data_system/streaming.rb +++ b/lib/ldclient-rb/impl/data_system/streaming.rb @@ -5,7 +5,7 @@ require "ldclient-rb/impl/data_system" require "ldclient-rb/impl/data_system/protocolv2" require "ldclient-rb/impl/data_system/polling" # For shared constants -require "ldclient-rb/impl/data_system/data_source_builder_common" +require "ldclient-rb/data_system/data_source_builder_common" require "ldclient-rb/impl/util" require "concurrent" require "json" From 048d17b9a113c9d9d99892298bfbf48fae3b8aeb Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Thu, 5 Mar 2026 09:50:20 -0500 Subject: [PATCH 5/5] break data_system.rb up into separate files --- lib/ldclient-rb/config.rb | 8 +- lib/ldclient-rb/data_system.rb | 362 +----------------- lib/ldclient-rb/data_system/config_builder.rb | 104 +++++ .../polling_data_source_builder.rb | 227 +++++++++++ .../streaming_data_source_builder.rb | 73 ++++ lib/ldclient-rb/impl/data_system/polling.rb | 31 +- lib/ldclient-rb/impl/data_system/streaming.rb | 8 +- .../data_system/polling_initializer_spec.rb | 4 +- .../data_system/polling_synchronizer_spec.rb | 4 +- 9 files changed, 419 insertions(+), 402 deletions(-) create mode 100644 lib/ldclient-rb/data_system/config_builder.rb create mode 100644 lib/ldclient-rb/data_system/polling_data_source_builder.rb create mode 100644 lib/ldclient-rb/data_system/streaming_data_source_builder.rb diff --git a/lib/ldclient-rb/config.rb b/lib/ldclient-rb/config.rb index 6d123938..55a676ad 100644 --- a/lib/ldclient-rb/config.rb +++ b/lib/ldclient-rb/config.rb @@ -468,7 +468,7 @@ def self.default_capacity # @return [String] "https://sdk.launchdarkly.com" # def self.default_base_uri - Impl::DataSystem::DEFAULT_POLLING_BASE_URI + DataSystem::PollingDataSourceBuilder::DEFAULT_BASE_URI end # @@ -476,7 +476,7 @@ def self.default_base_uri # @return [String] "https://stream.launchdarkly.com" # def self.default_stream_uri - Impl::DataSystem::DEFAULT_STREAMING_BASE_URI + DataSystem::StreamingDataSourceBuilder::DEFAULT_BASE_URI end # @@ -516,7 +516,7 @@ def self.default_read_timeout # @return [Float] 1 # def self.default_initial_reconnect_delay - Impl::DataSystem::DEFAULT_INITIAL_RECONNECT_DELAY + DataSystem::StreamingDataSourceBuilder::DEFAULT_INITIAL_RECONNECT_DELAY end # @@ -578,7 +578,7 @@ def self.default_offline # @return [Float] 30 # def self.default_poll_interval - Impl::DataSystem::DEFAULT_POLL_INTERVAL + DataSystem::PollingDataSourceBuilder::DEFAULT_POLL_INTERVAL end # diff --git a/lib/ldclient-rb/data_system.rb b/lib/ldclient-rb/data_system.rb index 961dbb9d..dd03b511 100644 --- a/lib/ldclient-rb/data_system.rb +++ b/lib/ldclient-rb/data_system.rb @@ -4,6 +4,9 @@ require 'ldclient-rb/config' require 'ldclient-rb/impl/data_system/polling' require 'ldclient-rb/impl/data_system/streaming' +require 'ldclient-rb/data_system/config_builder' +require 'ldclient-rb/data_system/polling_data_source_builder' +require 'ldclient-rb/data_system/streaming_data_source_builder' module LaunchDarkly # @@ -52,365 +55,6 @@ module LaunchDarkly # config = LaunchDarkly::Config.new(data_system: data_system) # module DataSystem - # - # Interface for custom polling requesters. - # - # A Requester is responsible for fetching data from a data source. The SDK - # ships with built-in HTTP requesters for both FDv2 and FDv1 polling endpoints, - # but you can implement this interface to provide custom data fetching logic - # (e.g., reading from a file, a database, or a custom API). - # - # == Implementing a Custom Requester - # - # To create a custom requester, include this module and implement the {#fetch} - # method: - # - # class MyCustomRequester - # include LaunchDarkly::DataSystem::Requester - # - # def fetch(selector) - # # Fetch data and return a Result containing [ChangeSet, headers] - # # ... - # LaunchDarkly::Result.success([change_set, {}]) - # end - # - # def stop - # # Clean up resources - # end - # end - # - # polling = LaunchDarkly::DataSystem.polling_ds_builder - # .requester(MyCustomRequester.new) - # - # @see PollingDataSourceBuilder#requester - # - Requester = LaunchDarkly::Impl::DataSystem::Requester - - # - # Builder for the data system configuration. - # - # This builder configures the overall data acquisition strategy for the SDK, - # including which data sources to use for initialization and synchronization, - # and how to interact with a persistent data store. - # - # @see DataSystem.default - # @see DataSystem.streaming - # @see DataSystem.polling - # @see DataSystem.custom - # - class ConfigBuilder - def initialize - @initializers = nil - @synchronizers = nil - @fdv1_fallback_synchronizer = nil - @data_store_mode = LaunchDarkly::Interfaces::DataSystem::DataStoreMode::READ_ONLY - @data_store = nil - end - - # - # Sets the initializers for the data system. - # - # Initializers are used to fetch an initial set of data when the SDK starts. - # They are tried in order; if the first one fails, the next is tried, and so on. - # - # @param initializers [Array<#build(String, Config)>] - # Array of builders that respond to build(sdk_key, config) and return an Initializer - # @return [ConfigBuilder] self for chaining - # - def initializers(initializers) - @initializers = initializers - self - end - - # - # Sets the synchronizers for the data system. - # - # Synchronizers keep data up-to-date after initialization. Like initializers, - # they are tried in order. If the primary synchronizer fails, the next one - # takes over. - # - # @param synchronizers [Array<#build(String, Config)>] - # Array of builders that respond to build(sdk_key, config) and return a Synchronizer - # @return [ConfigBuilder] self for chaining - # - def synchronizers(synchronizers) - @synchronizers = synchronizers - self - end - - # - # Configures the SDK with a fallback synchronizer that is compatible with - # the Flag Delivery v1 API. - # - # This fallback is used when the server signals that the environment should - # revert to FDv1 protocol. Most users will not need to set this directly. - # - # @param fallback [#build(String, Config)] Builder that responds to build(sdk_key, config) and returns the fallback Synchronizer - # @return [ConfigBuilder] self for chaining - # - def fdv1_compatible_synchronizer(fallback) - @fdv1_fallback_synchronizer = fallback - self - end - - # - # Sets the data store configuration for the data system. - # - # @param data_store [LaunchDarkly::Interfaces::FeatureStore] The data store - # @param store_mode [Symbol] The store mode (use constants from - # {LaunchDarkly::Interfaces::DataSystem::DataStoreMode}) - # @return [ConfigBuilder] self for chaining - # - def data_store(data_store, store_mode) - @data_store = data_store - @data_store_mode = store_mode - self - end - - # - # Builds the data system configuration. - # - # @return [DataSystemConfig] - # - def build - DataSystemConfig.new( - initializers: @initializers, - synchronizers: @synchronizers, - data_store_mode: @data_store_mode, - data_store: @data_store, - fdv1_fallback_synchronizer: @fdv1_fallback_synchronizer - ) - end - end - - # - # Builder for a polling data source that communicates with LaunchDarkly's - # FDv2 polling endpoint. - # - # This builder can be used with {ConfigBuilder#initializers} or - # {ConfigBuilder#synchronizers} to create custom data system configurations. - # - # The polling data source periodically fetches data from LaunchDarkly. It - # supports conditional requests via ETags, so subsequent polls after the - # initial request only transfer data if changes have occurred. - # - # == Example - # - # polling = LaunchDarkly::DataSystem.polling_ds_builder - # .poll_interval(60) - # .base_uri("https://custom-endpoint.example.com") - # - # data_system = LaunchDarkly::DataSystem.custom - # .synchronizers([polling]) - # - # @see DataSystem.polling_ds_builder - # - class PollingDataSourceBuilder - include LaunchDarkly::DataSystem::DataSourceBuilderCommon - - # @return [String] The default base URI for polling requests - DEFAULT_BASE_URI = LaunchDarkly::Impl::DataSystem::DEFAULT_POLLING_BASE_URI - - # @return [Float] The default polling interval in seconds - DEFAULT_POLL_INTERVAL = LaunchDarkly::Impl::DataSystem::DEFAULT_POLL_INTERVAL - - def initialize - @requester = nil - end - - # - # Sets the polling interval in seconds. - # - # This controls how frequently the SDK polls LaunchDarkly for updates. - # Lower values mean more frequent updates but higher network traffic. - # The default is {DEFAULT_POLL_INTERVAL} seconds. - # - # @param secs [Float] Polling interval in seconds - # @return [PollingDataSourceBuilder] self for chaining - # - def poll_interval(secs) - @poll_interval = secs - self - end - - # - # Sets a custom {Requester} for this polling data source. - # - # By default, the builder uses an HTTP requester that communicates with - # LaunchDarkly's FDv2 polling endpoint. Use this method to provide a - # custom requester implementation for testing or non-standard environments. - # - # @param requester [Requester] A custom requester that implements the - # {Requester} interface - # @return [PollingDataSourceBuilder] self for chaining - # - # @see Requester - # - def requester(requester) - @requester = requester - self - end - - # - # Builds the polling data source with the configured parameters. - # - # This method is called internally by the SDK. You do not need to call it - # directly; instead, pass the builder to {ConfigBuilder#initializers} or - # {ConfigBuilder#synchronizers}. - # - # @param sdk_key [String] The SDK key - # @param config [LaunchDarkly::Config] The SDK configuration - # @return [LaunchDarkly::Impl::DataSystem::PollingDataSource] - # - def build(sdk_key, config) - http_opts = build_http_config - requester = @requester || LaunchDarkly::Impl::DataSystem::HTTPPollingRequester.new(sdk_key, http_opts, config) - LaunchDarkly::Impl::DataSystem::PollingDataSource.new(@poll_interval || DEFAULT_POLL_INTERVAL, requester, config.logger) - end - end - - # - # Builder for a polling data source that communicates with LaunchDarkly's - # FDv1 (Flag Delivery v1) polling endpoint. - # - # This builder is typically used with {ConfigBuilder#fdv1_compatible_synchronizer} - # to provide a fallback when the server signals that the environment should - # revert to the FDv1 protocol. - # - # Most users will not need to interact with this builder directly, as the - # predefined strategies ({DataSystem.default}, {DataSystem.streaming}, - # {DataSystem.polling}) already configure an appropriate FDv1 fallback. - # - # @see DataSystem.fdv1_fallback_ds_builder - # - class FDv1PollingDataSourceBuilder - include LaunchDarkly::DataSystem::DataSourceBuilderCommon - - # @return [String] The default base URI for FDv1 polling requests - DEFAULT_BASE_URI = LaunchDarkly::Impl::DataSystem::DEFAULT_POLLING_BASE_URI - - # @return [Float] The default polling interval in seconds - DEFAULT_POLL_INTERVAL = LaunchDarkly::Impl::DataSystem::DEFAULT_POLL_INTERVAL - - def initialize - @requester = nil - end - - # - # Sets the polling interval in seconds. - # - # This controls how frequently the SDK polls LaunchDarkly for updates. - # The default is {DEFAULT_POLL_INTERVAL} seconds. - # - # @param secs [Float] Polling interval in seconds - # @return [FDv1PollingDataSourceBuilder] self for chaining - # - def poll_interval(secs) - @poll_interval = secs - self - end - - # - # Sets a custom {Requester} for this polling data source. - # - # By default, the builder uses an HTTP requester that communicates with - # LaunchDarkly's FDv1 polling endpoint. Use this method to provide a - # custom requester implementation. - # - # @param requester [Requester] A custom requester that implements the - # {Requester} interface - # @return [FDv1PollingDataSourceBuilder] self for chaining - # - # @see Requester - # - def requester(requester) - @requester = requester - self - end - - # - # Builds the FDv1 polling data source with the configured parameters. - # - # This method is called internally by the SDK. You do not need to call it - # directly; instead, pass the builder to {ConfigBuilder#fdv1_compatible_synchronizer}. - # - # @param sdk_key [String] The SDK key - # @param config [LaunchDarkly::Config] The SDK configuration - # @return [LaunchDarkly::Impl::DataSystem::PollingDataSource] - # - def build(sdk_key, config) - http_opts = build_http_config - requester = @requester || LaunchDarkly::Impl::DataSystem::HTTPFDv1PollingRequester.new(sdk_key, http_opts, config) - LaunchDarkly::Impl::DataSystem::PollingDataSource.new(@poll_interval || DEFAULT_POLL_INTERVAL, requester, config.logger) - end - end - - # - # Builder for a streaming data source that uses Server-Sent Events (SSE) - # to receive real-time updates from LaunchDarkly's Flag Delivery services. - # - # This builder can be used with {ConfigBuilder#synchronizers} to create - # custom data system configurations. Streaming provides the lowest latency - # for flag updates compared to polling. - # - # == Example - # - # streaming = LaunchDarkly::DataSystem.streaming_ds_builder - # .initial_reconnect_delay(2) - # .base_uri("https://custom-stream.example.com") - # - # data_system = LaunchDarkly::DataSystem.custom - # .synchronizers([streaming]) - # - # @see DataSystem.streaming_ds_builder - # - class StreamingDataSourceBuilder - include LaunchDarkly::DataSystem::DataSourceBuilderCommon - - # @return [String] The default base URI for streaming connections - DEFAULT_BASE_URI = LaunchDarkly::Impl::DataSystem::DEFAULT_STREAMING_BASE_URI - - # @return [Float] The default initial reconnect delay in seconds - DEFAULT_INITIAL_RECONNECT_DELAY = LaunchDarkly::Impl::DataSystem::DEFAULT_INITIAL_RECONNECT_DELAY - - def initialize - # No initialization needed - defaults applied in build via nil-check - end - - # - # Sets the initial delay before reconnecting after a stream connection error. - # - # The SDK uses an exponential backoff strategy starting from this delay. - # The default is {DEFAULT_INITIAL_RECONNECT_DELAY} second. - # - # @param delay [Float] Delay in seconds - # @return [StreamingDataSourceBuilder] self for chaining - # - def initial_reconnect_delay(delay) - @initial_reconnect_delay = delay - self - end - - # - # Builds the streaming data source with the configured parameters. - # - # This method is called internally by the SDK. You do not need to call it - # directly; instead, pass the builder to {ConfigBuilder#synchronizers}. - # - # @param sdk_key [String] The SDK key - # @param config [LaunchDarkly::Config] The SDK configuration - # @return [LaunchDarkly::Impl::DataSystem::StreamingDataSource] - # - def build(sdk_key, config) - http_opts = build_http_config - LaunchDarkly::Impl::DataSystem::StreamingDataSource.new( - sdk_key, http_opts, - @initial_reconnect_delay || DEFAULT_INITIAL_RECONNECT_DELAY, - config - ) - end - end - # # Returns a builder for creating a polling data source. # This is a building block that can be used with {ConfigBuilder#initializers} diff --git a/lib/ldclient-rb/data_system/config_builder.rb b/lib/ldclient-rb/data_system/config_builder.rb new file mode 100644 index 00000000..43dced05 --- /dev/null +++ b/lib/ldclient-rb/data_system/config_builder.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require "ldclient-rb/interfaces/data_system" + +module LaunchDarkly + module DataSystem + # + # Builder for the data system configuration. + # + # This builder configures the overall data acquisition strategy for the SDK, + # including which data sources to use for initialization and synchronization, + # and how to interact with a persistent data store. + # + # @see DataSystem.default + # @see DataSystem.streaming + # @see DataSystem.polling + # @see DataSystem.custom + # + class ConfigBuilder + def initialize + @initializers = nil + @synchronizers = nil + @fdv1_fallback_synchronizer = nil + @data_store_mode = LaunchDarkly::Interfaces::DataSystem::DataStoreMode::READ_ONLY + @data_store = nil + end + + # + # Sets the initializers for the data system. + # + # Initializers are used to fetch an initial set of data when the SDK starts. + # They are tried in order; if the first one fails, the next is tried, and so on. + # + # @param initializers [Array<#build(String, Config)>] + # Array of builders that respond to build(sdk_key, config) and return an Initializer + # @return [ConfigBuilder] self for chaining + # + def initializers(initializers) + @initializers = initializers + self + end + + # + # Sets the synchronizers for the data system. + # + # Synchronizers keep data up-to-date after initialization. Like initializers, + # they are tried in order. If the primary synchronizer fails, the next one + # takes over. + # + # @param synchronizers [Array<#build(String, Config)>] + # Array of builders that respond to build(sdk_key, config) and return a Synchronizer + # @return [ConfigBuilder] self for chaining + # + def synchronizers(synchronizers) + @synchronizers = synchronizers + self + end + + # + # Configures the SDK with a fallback synchronizer that is compatible with + # the Flag Delivery v1 API. + # + # This fallback is used when the server signals that the environment should + # revert to FDv1 protocol. Most users will not need to set this directly. + # + # @param fallback [#build(String, Config)] Builder that responds to build(sdk_key, config) and returns the fallback Synchronizer + # @return [ConfigBuilder] self for chaining + # + def fdv1_compatible_synchronizer(fallback) + @fdv1_fallback_synchronizer = fallback + self + end + + # + # Sets the data store configuration for the data system. + # + # @param data_store [LaunchDarkly::Interfaces::FeatureStore] The data store + # @param store_mode [Symbol] The store mode (use constants from + # {LaunchDarkly::Interfaces::DataSystem::DataStoreMode}) + # @return [ConfigBuilder] self for chaining + # + def data_store(data_store, store_mode) + @data_store = data_store + @data_store_mode = store_mode + self + end + + # + # Builds the data system configuration. + # + # @return [DataSystemConfig] + # + def build + DataSystemConfig.new( + initializers: @initializers, + synchronizers: @synchronizers, + data_store_mode: @data_store_mode, + data_store: @data_store, + fdv1_fallback_synchronizer: @fdv1_fallback_synchronizer + ) + end + end + end +end diff --git a/lib/ldclient-rb/data_system/polling_data_source_builder.rb b/lib/ldclient-rb/data_system/polling_data_source_builder.rb new file mode 100644 index 00000000..e68a88db --- /dev/null +++ b/lib/ldclient-rb/data_system/polling_data_source_builder.rb @@ -0,0 +1,227 @@ +# frozen_string_literal: true + +require "ldclient-rb/data_system/data_source_builder_common" + +module LaunchDarkly + module DataSystem + # + # Interface for custom polling requesters. + # + # A Requester is responsible for fetching data from a data source. The SDK + # ships with built-in HTTP requesters for both FDv2 and FDv1 polling endpoints, + # but you can implement this interface to provide custom data fetching logic + # (e.g., reading from a file, a database, or a custom API). + # + # == Implementing a Custom Requester + # + # To create a custom requester, include this module and implement the {#fetch} + # method: + # + # class MyCustomRequester + # include LaunchDarkly::DataSystem::Requester + # + # def fetch(selector) + # # Fetch data and return a Result containing [ChangeSet, headers] + # # ... + # LaunchDarkly::Result.success([change_set, {}]) + # end + # + # def stop + # # Clean up resources + # end + # end + # + # polling = LaunchDarkly::DataSystem.polling_ds_builder + # .requester(MyCustomRequester.new) + # + # @see PollingDataSourceBuilder#requester + # + module Requester + # + # Fetches data for the given selector. + # + # @param selector [LaunchDarkly::Interfaces::DataSystem::Selector, nil] + # The selector describing what data to fetch. May be nil if no + # selector is available (e.g., on the first request). + # @return [LaunchDarkly::Result] A Result containing a tuple of + # [ChangeSet, headers] on success, or an error message on failure. + # + def fetch(selector) + raise NotImplementedError + end + + # + # Releases any resources held by this requester (e.g., persistent HTTP + # connections). Called when the requester is no longer needed. + # + # Implementations should handle being called multiple times gracefully. + # The default implementation is a no-op. + # + def stop + # Optional - implementations may override if they need cleanup + end + end + + # + # Builder for a polling data source that communicates with LaunchDarkly's + # FDv2 polling endpoint. + # + # This builder can be used with {ConfigBuilder#initializers} or + # {ConfigBuilder#synchronizers} to create custom data system configurations. + # + # The polling data source periodically fetches data from LaunchDarkly. It + # supports conditional requests via ETags, so subsequent polls after the + # initial request only transfer data if changes have occurred. + # + # == Example + # + # polling = LaunchDarkly::DataSystem.polling_ds_builder + # .poll_interval(60) + # .base_uri("https://custom-endpoint.example.com") + # + # data_system = LaunchDarkly::DataSystem.custom + # .synchronizers([polling]) + # + # @see DataSystem.polling_ds_builder + # + class PollingDataSourceBuilder + include LaunchDarkly::DataSystem::DataSourceBuilderCommon + + # @return [String] The default base URI for polling requests + DEFAULT_BASE_URI = "https://sdk.launchdarkly.com" + + # @return [Float] The default polling interval in seconds + DEFAULT_POLL_INTERVAL = 30 + + def initialize + @requester = nil + end + + # + # Sets the polling interval in seconds. + # + # This controls how frequently the SDK polls LaunchDarkly for updates. + # Lower values mean more frequent updates but higher network traffic. + # The default is {DEFAULT_POLL_INTERVAL} seconds. + # + # @param secs [Float] Polling interval in seconds + # @return [PollingDataSourceBuilder] self for chaining + # + def poll_interval(secs) + @poll_interval = secs + self + end + + # + # Sets a custom {Requester} for this polling data source. + # + # By default, the builder uses an HTTP requester that communicates with + # LaunchDarkly's FDv2 polling endpoint. Use this method to provide a + # custom requester implementation for testing or non-standard environments. + # + # @param requester [Requester] A custom requester that implements the + # {Requester} interface + # @return [PollingDataSourceBuilder] self for chaining + # + # @see Requester + # + def requester(requester) + @requester = requester + self + end + + # + # Builds the polling data source with the configured parameters. + # + # This method is called internally by the SDK. You do not need to call it + # directly; instead, pass the builder to {ConfigBuilder#initializers} or + # {ConfigBuilder#synchronizers}. + # + # @param sdk_key [String] The SDK key + # @param config [LaunchDarkly::Config] The SDK configuration + # @return [LaunchDarkly::Impl::DataSystem::PollingDataSource] + # + def build(sdk_key, config) + http_opts = build_http_config + requester = @requester || LaunchDarkly::Impl::DataSystem::HTTPPollingRequester.new(sdk_key, http_opts, config) + LaunchDarkly::Impl::DataSystem::PollingDataSource.new(@poll_interval || DEFAULT_POLL_INTERVAL, requester, config.logger) + end + end + + # + # Builder for a polling data source that communicates with LaunchDarkly's + # FDv1 (Flag Delivery v1) polling endpoint. + # + # This builder is typically used with {ConfigBuilder#fdv1_compatible_synchronizer} + # to provide a fallback when the server signals that the environment should + # revert to the FDv1 protocol. + # + # Most users will not need to interact with this builder directly, as the + # predefined strategies ({DataSystem.default}, {DataSystem.streaming}, + # {DataSystem.polling}) already configure an appropriate FDv1 fallback. + # + # @see DataSystem.fdv1_fallback_ds_builder + # + class FDv1PollingDataSourceBuilder + include LaunchDarkly::DataSystem::DataSourceBuilderCommon + + # @return [String] The default base URI for FDv1 polling requests + DEFAULT_BASE_URI = "https://sdk.launchdarkly.com" + + # @return [Float] The default polling interval in seconds + DEFAULT_POLL_INTERVAL = 30 + + def initialize + @requester = nil + end + + # + # Sets the polling interval in seconds. + # + # This controls how frequently the SDK polls LaunchDarkly for updates. + # The default is {DEFAULT_POLL_INTERVAL} seconds. + # + # @param secs [Float] Polling interval in seconds + # @return [FDv1PollingDataSourceBuilder] self for chaining + # + def poll_interval(secs) + @poll_interval = secs + self + end + + # + # Sets a custom {Requester} for this polling data source. + # + # By default, the builder uses an HTTP requester that communicates with + # LaunchDarkly's FDv1 polling endpoint. Use this method to provide a + # custom requester implementation. + # + # @param requester [Requester] A custom requester that implements the + # {Requester} interface + # @return [FDv1PollingDataSourceBuilder] self for chaining + # + # @see Requester + # + def requester(requester) + @requester = requester + self + end + + # + # Builds the FDv1 polling data source with the configured parameters. + # + # This method is called internally by the SDK. You do not need to call it + # directly; instead, pass the builder to {ConfigBuilder#fdv1_compatible_synchronizer}. + # + # @param sdk_key [String] The SDK key + # @param config [LaunchDarkly::Config] The SDK configuration + # @return [LaunchDarkly::Impl::DataSystem::PollingDataSource] + # + def build(sdk_key, config) + http_opts = build_http_config + requester = @requester || LaunchDarkly::Impl::DataSystem::HTTPFDv1PollingRequester.new(sdk_key, http_opts, config) + LaunchDarkly::Impl::DataSystem::PollingDataSource.new(@poll_interval || DEFAULT_POLL_INTERVAL, requester, config.logger) + end + end + end +end diff --git a/lib/ldclient-rb/data_system/streaming_data_source_builder.rb b/lib/ldclient-rb/data_system/streaming_data_source_builder.rb new file mode 100644 index 00000000..7fdcf44b --- /dev/null +++ b/lib/ldclient-rb/data_system/streaming_data_source_builder.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require "ldclient-rb/data_system/data_source_builder_common" + +module LaunchDarkly + module DataSystem + # + # Builder for a streaming data source that uses Server-Sent Events (SSE) + # to receive real-time updates from LaunchDarkly's Flag Delivery services. + # + # This builder can be used with {ConfigBuilder#synchronizers} to create + # custom data system configurations. Streaming provides the lowest latency + # for flag updates compared to polling. + # + # == Example + # + # streaming = LaunchDarkly::DataSystem.streaming_ds_builder + # .initial_reconnect_delay(2) + # .base_uri("https://custom-stream.example.com") + # + # data_system = LaunchDarkly::DataSystem.custom + # .synchronizers([streaming]) + # + # @see DataSystem.streaming_ds_builder + # + class StreamingDataSourceBuilder + include LaunchDarkly::DataSystem::DataSourceBuilderCommon + + # @return [String] The default base URI for streaming connections + DEFAULT_BASE_URI = "https://stream.launchdarkly.com" + + # @return [Float] The default initial reconnect delay in seconds + DEFAULT_INITIAL_RECONNECT_DELAY = 1 + + def initialize + # No initialization needed - defaults applied in build via nil-check + end + + # + # Sets the initial delay before reconnecting after a stream connection error. + # + # The SDK uses an exponential backoff strategy starting from this delay. + # The default is {DEFAULT_INITIAL_RECONNECT_DELAY} second. + # + # @param delay [Float] Delay in seconds + # @return [StreamingDataSourceBuilder] self for chaining + # + def initial_reconnect_delay(delay) + @initial_reconnect_delay = delay + self + end + + # + # Builds the streaming data source with the configured parameters. + # + # This method is called internally by the SDK. You do not need to call it + # directly; instead, pass the builder to {ConfigBuilder#synchronizers}. + # + # @param sdk_key [String] The SDK key + # @param config [LaunchDarkly::Config] The SDK configuration + # @return [LaunchDarkly::Impl::DataSystem::StreamingDataSource] + # + def build(sdk_key, config) + http_opts = build_http_config + LaunchDarkly::Impl::DataSystem::StreamingDataSource.new( + sdk_key, http_opts, + @initial_reconnect_delay || DEFAULT_INITIAL_RECONNECT_DELAY, + config + ) + end + end + end +end diff --git a/lib/ldclient-rb/impl/data_system/polling.rb b/lib/ldclient-rb/impl/data_system/polling.rb index 9f54e3ee..fe680f54 100644 --- a/lib/ldclient-rb/impl/data_system/polling.rb +++ b/lib/ldclient-rb/impl/data_system/polling.rb @@ -4,7 +4,7 @@ require "ldclient-rb/interfaces/data_system" require "ldclient-rb/impl/data_system" require "ldclient-rb/impl/data_system/protocolv2" -require "ldclient-rb/data_system/data_source_builder_common" +require "ldclient-rb/data_system/polling_data_source_builder" require "ldclient-rb/impl/data_source/requestor" require "ldclient-rb/impl/util" require "concurrent" @@ -21,31 +21,6 @@ module DataSystem LD_ENVID_HEADER = "X-LD-EnvID" LD_FD_FALLBACK_HEADER = "X-LD-FD-Fallback" - # Default base URI for polling requests. - DEFAULT_POLLING_BASE_URI = "https://sdk.launchdarkly.com" - - # Default polling interval in seconds. - DEFAULT_POLL_INTERVAL = 30 - - # - # @deprecated Use {LaunchDarkly::DataSystem::Requester} instead. This module - # remains here for backward compatibility and for use by internal classes - # that are loaded before the public module. - # - # @see LaunchDarkly::DataSystem::Requester - # - module Requester - # @see LaunchDarkly::DataSystem::Requester#fetch - def fetch(selector) - raise NotImplementedError - end - - # @see LaunchDarkly::DataSystem::Requester#stop - def stop - # Optional - implementations may override if they need cleanup - end - end - # # PollingDataSource is a data source that can retrieve information from # LaunchDarkly either as an Initializer or as a Synchronizer. @@ -245,7 +220,7 @@ def stop # requests to the FDv2 polling endpoint. # class HTTPPollingRequester - include Requester + include LaunchDarkly::DataSystem::Requester # # @param sdk_key [String] @@ -337,7 +312,7 @@ def stop # requests to the FDv1 polling endpoint. # class HTTPFDv1PollingRequester - include Requester + include LaunchDarkly::DataSystem::Requester # # @param sdk_key [String] diff --git a/lib/ldclient-rb/impl/data_system/streaming.rb b/lib/ldclient-rb/impl/data_system/streaming.rb index 5eb8127d..05a39e47 100644 --- a/lib/ldclient-rb/impl/data_system/streaming.rb +++ b/lib/ldclient-rb/impl/data_system/streaming.rb @@ -5,7 +5,7 @@ require "ldclient-rb/impl/data_system" require "ldclient-rb/impl/data_system/protocolv2" require "ldclient-rb/impl/data_system/polling" # For shared constants -require "ldclient-rb/data_system/data_source_builder_common" +require "ldclient-rb/data_system/streaming_data_source_builder" require "ldclient-rb/impl/util" require "concurrent" require "json" @@ -21,12 +21,6 @@ module DataSystem # The heartbeats sent as comments on the stream will keep this from triggering. STREAM_READ_TIMEOUT = 5 * 60 - # Default base URI for streaming connections. - DEFAULT_STREAMING_BASE_URI = "https://stream.launchdarkly.com" - - # Default initial delay before reconnecting after an error, in seconds. - DEFAULT_INITIAL_RECONNECT_DELAY = 1 - # # StreamingDataSource is a Synchronizer that uses Server-Sent Events (SSE) # to receive real-time updates from LaunchDarkly's Flag Delivery services. diff --git a/spec/impl/data_system/polling_initializer_spec.rb b/spec/impl/data_system/polling_initializer_spec.rb index c94d9fdb..dc699c36 100644 --- a/spec/impl/data_system/polling_initializer_spec.rb +++ b/spec/impl/data_system/polling_initializer_spec.rb @@ -11,7 +11,7 @@ module DataSystem let(:logger) { double("Logger", info: nil, warn: nil, error: nil, debug: nil) } class MockExceptionThrowingPollingRequester - include Requester + include LaunchDarkly::DataSystem::Requester def fetch(selector) raise "This is a mock exception for testing purposes." @@ -19,7 +19,7 @@ def fetch(selector) end class MockPollingRequester - include Requester + include LaunchDarkly::DataSystem::Requester def initialize(result) @result = result diff --git a/spec/impl/data_system/polling_synchronizer_spec.rb b/spec/impl/data_system/polling_synchronizer_spec.rb index c4fca3c8..f7d9031e 100644 --- a/spec/impl/data_system/polling_synchronizer_spec.rb +++ b/spec/impl/data_system/polling_synchronizer_spec.rb @@ -11,7 +11,7 @@ module DataSystem let(:logger) { double("Logger", info: nil, warn: nil, error: nil, debug: nil) } class ListBasedRequester - include Requester + include LaunchDarkly::DataSystem::Requester def initialize(results) @results = results @@ -24,7 +24,7 @@ def fetch(selector) end class RequesterWithCleanup - include Requester + include LaunchDarkly::DataSystem::Requester attr_reader :stop_called