-
Notifications
You must be signed in to change notification settings - Fork 55
chore: Add the FDv2 data system protocol implementation #346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jsonbailey
wants to merge
9
commits into
jb/sdk-1541/convert-client-to-datasystem
Choose a base branch
from
jb/sdk-1542/fdv2-datasystem-protocol-implementation
base: jb/sdk-1541/convert-client-to-datasystem
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
4bdc298
chore: Add the FDv2 data system protocol implementation
jsonbailey 929995d
Apply suggestion from @keelerm84
jsonbailey bb46c60
Apply suggestion from @keelerm84
jsonbailey 4794483
Apply suggestion from @keelerm84
jsonbailey 513fafe
address feedback on pr
jsonbailey b43c904
ensure consistency for keys being strings
jsonbailey 467c120
fix invalid log reference
jsonbailey ff982dd
fix method names and simplify code
jsonbailey 0250999
We should expect symbols, simplify the code and remove error with boo…
jsonbailey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'ldclient-rb/interfaces/data_system' | ||
| require 'ldclient-rb/config' | ||
|
|
||
| module LaunchDarkly | ||
| # | ||
| # Configuration for LaunchDarkly's data acquisition strategy. | ||
| # | ||
| # This module provides factory methods for creating data system configurations. | ||
| # | ||
| module DataSystem | ||
| # | ||
| # Builder for the data system configuration. | ||
| # | ||
| class ConfigBuilder | ||
| def initialize | ||
| @initializers = nil | ||
| @primary_synchronizer = nil | ||
| @secondary_synchronizer = nil | ||
| @fdv1_fallback_synchronizer = nil | ||
| @data_store_mode = LaunchDarkly::Interfaces::DataStoreMode::READ_ONLY | ||
| @data_store = nil | ||
| end | ||
|
|
||
| # | ||
| # Sets the initializers for the data system. | ||
| # | ||
| # @param initializers [Array<Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Initializer>] | ||
| # Array of builder procs that take sdk_key and Config and return an Initializer | ||
| # @return [ConfigBuilder] self for chaining | ||
| # | ||
| def initializers(initializers) | ||
| @initializers = initializers | ||
| self | ||
| end | ||
|
|
||
| # | ||
| # Sets the synchronizers for the data system. | ||
| # | ||
| # @param primary [Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer] Builder proc that takes sdk_key and Config and returns the primary Synchronizer | ||
| # @param secondary [Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil] | ||
| # Builder proc that takes sdk_key and Config and returns the secondary Synchronizer | ||
| # @return [ConfigBuilder] self for chaining | ||
| # | ||
| def synchronizers(primary, secondary = nil) | ||
| @primary_synchronizer = primary | ||
| @secondary_synchronizer = secondary | ||
| self | ||
| end | ||
|
|
||
| # | ||
| # Configures the SDK with a fallback synchronizer that is compatible with | ||
| # the Flag Delivery v1 API. | ||
| # | ||
| # @param fallback [Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer] | ||
| # Builder proc that takes sdk_key and 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 | ||
| # @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] | ||
| # @raise [ArgumentError] if configuration is invalid | ||
| # | ||
| def build | ||
| if @secondary_synchronizer && @primary_synchronizer.nil? | ||
| raise ArgumentError, "Primary synchronizer must be set if secondary is set" | ||
| end | ||
|
|
||
| DataSystemConfig.new( | ||
| initializers: @initializers, | ||
| primary_synchronizer: @primary_synchronizer, | ||
| secondary_synchronizer: @secondary_synchronizer, | ||
| data_store_mode: @data_store_mode, | ||
| data_store: @data_store, | ||
| fdv1_fallback_synchronizer: @fdv1_fallback_synchronizer | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| # @private | ||
| def self.polling_ds_builder | ||
| # TODO(fdv2): Implement polling data source builder | ||
| lambda do |_sdk_key, _config| | ||
| raise NotImplementedError, "Polling data source not yet implemented for FDv2" | ||
| end | ||
| end | ||
|
|
||
| # @private | ||
| def self.fdv1_fallback_ds_builder | ||
| # TODO(fdv2): Implement FDv1 fallback polling data source builder | ||
| lambda do |_sdk_key, _config| | ||
| raise NotImplementedError, "FDv1 fallback data source not yet implemented for FDv2" | ||
| end | ||
| end | ||
|
|
||
| # @private | ||
| def self.streaming_ds_builder | ||
| # TODO(fdv2): Implement streaming data source builder | ||
| lambda do |_sdk_key, _config| | ||
| raise NotImplementedError, "Streaming data source not yet implemented for FDv2" | ||
| end | ||
| end | ||
|
|
||
| # | ||
| # Default is LaunchDarkly's recommended flag data acquisition strategy. | ||
| # | ||
| # Currently, it operates a two-phase method for obtaining data: first, it | ||
| # requests data from LaunchDarkly's global CDN. Then, it initiates a | ||
| # streaming connection to LaunchDarkly's Flag Delivery services to | ||
| # receive real-time updates. | ||
| # | ||
| # If the streaming connection is interrupted for an extended period of | ||
| # time, the SDK will automatically fall back to polling the global CDN | ||
| # for updates. | ||
| # | ||
| # @return [ConfigBuilder] | ||
| # | ||
| def self.default | ||
| polling_builder = polling_ds_builder | ||
| streaming_builder = streaming_ds_builder | ||
| fallback = fdv1_fallback_ds_builder | ||
|
|
||
| builder = ConfigBuilder.new | ||
| builder.initializers([polling_builder]) | ||
| builder.synchronizers(streaming_builder, polling_builder) | ||
| builder.fdv1_compatible_synchronizer(fallback) | ||
|
|
||
| builder | ||
| end | ||
|
|
||
| # | ||
| # Streaming configures the SDK to efficiently stream flag/segment data | ||
| # in the background, allowing evaluations to operate on the latest data | ||
| # with no additional latency. | ||
| # | ||
| # @return [ConfigBuilder] | ||
| # | ||
| def self.streaming | ||
| streaming_builder = streaming_ds_builder | ||
| fallback = fdv1_fallback_ds_builder | ||
|
|
||
| builder = ConfigBuilder.new | ||
| builder.synchronizers(streaming_builder) | ||
| builder.fdv1_compatible_synchronizer(fallback) | ||
|
|
||
| builder | ||
| end | ||
|
|
||
| # | ||
| # Polling configures the SDK to regularly poll an endpoint for | ||
| # flag/segment data in the background. This is less efficient than | ||
| # streaming, but may be necessary in some network environments. | ||
| # | ||
| # @return [ConfigBuilder] | ||
| # | ||
| def self.polling | ||
| polling_builder = polling_ds_builder | ||
| fallback = fdv1_fallback_ds_builder | ||
|
|
||
| builder = ConfigBuilder.new | ||
| builder.synchronizers(polling_builder) | ||
| builder.fdv1_compatible_synchronizer(fallback) | ||
|
|
||
| builder | ||
| end | ||
|
|
||
| # | ||
| # Custom returns a builder suitable for creating a custom data | ||
| # acquisition strategy. You may configure how the SDK uses a Persistent | ||
| # Store, how the SDK obtains an initial set of data, and how the SDK | ||
| # keeps data up-to-date. | ||
| # | ||
| # @return [ConfigBuilder] | ||
| # | ||
| def self.custom | ||
| ConfigBuilder.new | ||
| end | ||
|
|
||
| # | ||
| # Daemon configures the SDK to read from a persistent store integration | ||
| # that is populated by Relay Proxy or other SDKs. The SDK will not connect | ||
| # to LaunchDarkly. In this mode, the SDK never writes to the data store. | ||
| # | ||
| # @param store [Object] The persistent store | ||
| # @return [ConfigBuilder] | ||
| # | ||
| def self.daemon(store) | ||
| custom.data_store(store, LaunchDarkly::Interfaces::DataStoreMode::READ_ONLY) | ||
| end | ||
|
|
||
| # | ||
| # PersistentStore is similar to default, with the addition of a persistent | ||
| # store integration. Before data has arrived from LaunchDarkly, the SDK is | ||
| # able to evaluate flags using data from the persistent store. Once fresh | ||
| # data is available, the SDK will no longer read from the persistent store, | ||
| # although it will keep it up-to-date. | ||
| # | ||
| # @param store [Object] The persistent store | ||
| # @return [ConfigBuilder] | ||
| # | ||
| def self.persistent_store(store) | ||
| default.data_store(store, LaunchDarkly::Interfaces::DataStoreMode::READ_WRITE) | ||
| end | ||
| end | ||
| end | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require "concurrent" | ||
| require "forwardable" | ||
| require "ldclient-rb/interfaces" | ||
|
|
||
| module LaunchDarkly | ||
| module Impl | ||
| module DataSource | ||
| # | ||
| # Provides status tracking and listener management for data sources. | ||
| # | ||
| # This class implements the {LaunchDarkly::Interfaces::DataSource::StatusProvider} interface. | ||
| # It maintains the current status of the data source and broadcasts status changes to listeners. | ||
| # | ||
| class StatusProviderV2 | ||
| include LaunchDarkly::Interfaces::DataSource::StatusProvider | ||
|
|
||
| extend Forwardable | ||
| def_delegators :@status_broadcaster, :add_listener, :remove_listener | ||
|
|
||
| # | ||
| # Creates a new status provider. | ||
| # | ||
| # @param status_broadcaster [LaunchDarkly::Impl::Broadcaster] Broadcaster for status changes | ||
| # | ||
| def initialize(status_broadcaster) | ||
| @status_broadcaster = status_broadcaster | ||
| @status = LaunchDarkly::Interfaces::DataSource::Status.new( | ||
| LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING, | ||
| Time.now, | ||
| nil | ||
| ) | ||
| @lock = Concurrent::ReadWriteLock.new | ||
| end | ||
|
|
||
| # (see LaunchDarkly::Interfaces::DataSource::StatusProvider#status) | ||
| def status | ||
| @lock.with_read_lock do | ||
| @status | ||
| end | ||
| end | ||
|
|
||
| # (see LaunchDarkly::Interfaces::DataSource::UpdateSink#update_status) | ||
| def update_status(new_state, new_error) | ||
| status_to_broadcast = nil | ||
|
|
||
| @lock.with_write_lock do | ||
| old_status = @status | ||
|
|
||
| # Special handling: INTERRUPTED during INITIALIZING stays INITIALIZING | ||
| if new_state == LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED && | ||
| old_status.state == LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING | ||
| new_state = LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING | ||
| end | ||
|
|
||
| # No change if state is the same and no error | ||
| return if new_state == old_status.state && new_error.nil? | ||
|
|
||
| new_since = new_state == old_status.state ? @status.state_since : Time.now | ||
| new_error = @status.last_error if new_error.nil? | ||
|
|
||
| @status = LaunchDarkly::Interfaces::DataSource::Status.new( | ||
| new_state, | ||
| new_since, | ||
| new_error | ||
| ) | ||
|
|
||
| status_to_broadcast = @status | ||
| end | ||
|
|
||
| @status_broadcaster.broadcast(status_to_broadcast) if status_to_broadcast | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.