Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions lib/ldclient-rb/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Config
# @option opts [Hash] :application See {#application}
# @option opts [String] :payload_filter_key See {#payload_filter_key}
# @option opts [Boolean] :omit_anonymous_contexts See {#omit_anonymous_contexts}
# @option opts [DataSystemConfig] :datasystem_config See {#datasystem_config}
# @option hooks [Array<Interfaces::Hooks::Hook]
# @option plugins [Array<Interfaces::Plugins::Plugin]
#
Expand Down Expand Up @@ -83,6 +84,7 @@ def initialize(opts = {})
@hooks = (opts[:hooks] || []).keep_if { |hook| hook.is_a? Interfaces::Hooks::Hook }
@plugins = (opts[:plugins] || []).keep_if { |plugin| plugin.is_a? Interfaces::Plugins::Plugin }
@omit_anonymous_contexts = opts.has_key?(:omit_anonymous_contexts) && opts[:omit_anonymous_contexts]
@datasystem_config = opts[:datasystem_config]
@data_source_update_sink = nil
@instance_id = nil
end
Expand Down Expand Up @@ -431,6 +433,15 @@ def diagnostic_opt_out?
#
attr_reader :omit_anonymous_contexts

#
# Configuration for the upcoming enhanced data system design. This is
# experimental and should not be set without direction from LaunchDarkly
# support.
#
# @return [DataSystemConfig, nil]
#
attr_reader :datasystem_config


#
# The default LaunchDarkly client configuration. This configuration sets
Expand Down Expand Up @@ -679,4 +690,55 @@ def initialize(store:, context_cache_size: nil, context_cache_time: nil, status_
# @return [Float]
attr_reader :stale_after
end

#
# Configuration for LaunchDarkly's data acquisition strategy.
#
# This is not stable and is not subject to any backwards compatibility guarantees
# or semantic versioning. It is not suitable for production usage.
#
class DataSystemConfig
#
# @param initializers [Array<Proc(Config) => LaunchDarkly::Interfaces::DataSystem::Initializer>, nil] The (optional) array of builder procs
# @param primary_synchronizer [Proc(Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil] The (optional) builder proc for primary synchronizer
# @param secondary_synchronizer [Proc(Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil] The (optional) builder proc for secondary synchronizer
# @param data_store_mode [Symbol] The (optional) data store mode
# @param data_store [LaunchDarkly::Interfaces::FeatureStore, nil] The (optional) data store
# @param fdv1_fallback_synchronizer [Proc(Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil]
# The (optional) builder proc for FDv1-compatible fallback synchronizer
#
def initialize(initializers: nil, primary_synchronizer: nil, secondary_synchronizer: nil,
data_store_mode: LaunchDarkly::Interfaces::DataStoreMode::READ_ONLY, data_store: nil, fdv1_fallback_synchronizer: nil)
@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

# The initializers for the data system. Each proc takes sdk_key and Config and returns an Initializer.
# @return [Array<Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Initializer>, nil]
attr_reader :initializers

# The primary synchronizer builder. Takes sdk_key and Config and returns a Synchronizer.
# @return [Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil]
attr_reader :primary_synchronizer

# The secondary synchronizer builder. Takes sdk_key and Config and returns a Synchronizer.
# @return [Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil]
attr_reader :secondary_synchronizer

# The data store mode.
# @return [Symbol]
attr_reader :data_store_mode

# The data store.
# @return [LaunchDarkly::Interfaces::FeatureStore, nil]
attr_reader :data_store

# The FDv1-compatible fallback synchronizer builder. Takes sdk_key and Config and returns a Synchronizer.
# @return [Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil]
attr_reader :fdv1_fallback_synchronizer
end
end
226 changes: 226 additions & 0 deletions lib/ldclient-rb/data_system.rb
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

78 changes: 78 additions & 0 deletions lib/ldclient-rb/impl/data_source/status_provider.rb
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

Loading