-
Notifications
You must be signed in to change notification settings - Fork 54
chore: Support running FDv2 contract tests #359
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
Changes from all commits
9e8e697
b42f005
b4a104c
0d73f85
d299b04
7ed5f6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,55 +14,99 @@ def initialize(log, config) | |
|
|
||
| opts[:logger] = log | ||
|
|
||
| if config[:streaming] | ||
| data_system_config = config[:dataSystem] | ||
| if data_system_config | ||
| data_system = LaunchDarkly::DataSystem.custom | ||
|
|
||
| # For FDv2, persistent store config is nested inside dataSystem.store | ||
| persistent_store_config = data_system_config.dig(:store, :persistentDataStore) | ||
| if persistent_store_config | ||
| store, store_mode = build_persistent_store(persistent_store_config) | ||
| data_system.data_store(store, store_mode) | ||
| end | ||
|
|
||
| init_configs = data_system_config[:initializers] | ||
| if init_configs | ||
| initializers = [] | ||
| init_configs.each do |init_config| | ||
| polling = init_config[:polling] | ||
| next unless polling | ||
|
|
||
| opts[:base_uri] = polling[:baseUri] if polling[:baseUri] | ||
| set_optional_time_prop(polling, :pollIntervalMs, opts, :poll_interval) | ||
| initializers << LaunchDarkly::DataSystem.polling_ds_builder | ||
| end | ||
| data_system.initializers(initializers) | ||
| end | ||
|
|
||
| sync_config = data_system_config[:synchronizers] | ||
| if sync_config | ||
| primary = sync_config[:primary] | ||
| secondary = sync_config[:secondary] | ||
|
|
||
| primary_builder = nil | ||
| secondary_builder = nil | ||
|
|
||
| if primary | ||
| streaming = primary[:streaming] | ||
| if streaming | ||
| opts[:stream_uri] = streaming[:baseUri] if streaming[:baseUri] | ||
| set_optional_time_prop(streaming, :initialRetryDelayMs, opts, :initial_reconnect_delay) | ||
| primary_builder = LaunchDarkly::DataSystem.streaming_ds_builder | ||
| elsif primary[:polling] | ||
| polling = primary[:polling] | ||
| opts[:base_uri] = polling[:baseUri] if polling[:baseUri] | ||
| set_optional_time_prop(polling, :pollIntervalMs, opts, :poll_interval) | ||
| primary_builder = LaunchDarkly::DataSystem.polling_ds_builder | ||
| end | ||
| end | ||
|
|
||
| if secondary | ||
| streaming = secondary[:streaming] | ||
| if streaming | ||
| opts[:stream_uri] = streaming[:baseUri] if streaming[:baseUri] | ||
| set_optional_time_prop(streaming, :initialRetryDelayMs, opts, :initial_reconnect_delay) | ||
| secondary_builder = LaunchDarkly::DataSystem.streaming_ds_builder | ||
| elsif secondary[:polling] | ||
| polling = secondary[:polling] | ||
| opts[:base_uri] = polling[:baseUri] if polling[:baseUri] | ||
| set_optional_time_prop(polling, :pollIntervalMs, opts, :poll_interval) | ||
| secondary_builder = LaunchDarkly::DataSystem.polling_ds_builder | ||
| end | ||
| end | ||
jsonbailey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| data_system.synchronizers(primary_builder, secondary_builder) if primary_builder | ||
jsonbailey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if primary_builder || secondary_builder | ||
| fallback_builder = LaunchDarkly::DataSystem.fdv1_fallback_ds_builder | ||
| data_system.fdv1_compatible_synchronizer(fallback_builder) | ||
| end | ||
| end | ||
|
|
||
| if data_system_config[:payloadFilter] | ||
| opts[:payload_filter_key] = data_system_config[:payloadFilter] | ||
| end | ||
|
|
||
| opts[:data_system_config] = data_system.build | ||
| elsif config[:streaming] | ||
| streaming = config[:streaming] | ||
| opts[:stream_uri] = streaming[:baseUri] unless streaming[:baseUri].nil? | ||
| opts[:payload_filter_key] = streaming[:filter] unless streaming[:filter].nil? | ||
| opts[:initial_reconnect_delay] = streaming[:initialRetryDelayMs] / 1_000.0 unless streaming[:initialRetryDelayMs].nil? | ||
| set_optional_time_prop(streaming, :initialRetryDelayMs, opts, :initial_reconnect_delay) | ||
| elsif config[:polling] | ||
| polling = config[:polling] | ||
| opts[:stream] = false | ||
| opts[:base_uri] = polling[:baseUri] unless polling[:baseUri].nil? | ||
| opts[:payload_filter_key] = polling[:filter] unless polling[:filter].nil? | ||
| opts[:poll_interval] = polling[:pollIntervalMs] / 1_000.0 unless polling[:pollIntervalMs].nil? | ||
| set_optional_time_prop(polling, :pollIntervalMs, opts, :poll_interval) | ||
| else | ||
| opts[:use_ldd] = true | ||
| end | ||
|
|
||
| if config[:persistentDataStore] | ||
| store_config = {} | ||
| store_config[:prefix] = config[:persistentDataStore][:store][:prefix] if config[:persistentDataStore][:store][:prefix] | ||
|
|
||
| case config[:persistentDataStore][:cache][:mode] | ||
| when 'off' | ||
| store_config[:expiration] = 0 | ||
| when 'infinite' | ||
| # NOTE: We don't actually support infinite cache mode, so we'll just set it to nil for now. This uses a default | ||
| # 15 second expiration time in the SDK, which is long enough to pass any test. | ||
| store_config[:expiration] = nil | ||
| when 'ttl' | ||
| store_config[:expiration] = config[:persistentDataStore][:cache][:ttl] | ||
| end | ||
|
|
||
| case config[:persistentDataStore][:store][:type] | ||
| when 'redis' | ||
| store_config[:redis_url] = config[:persistentDataStore][:store][:dsn] | ||
| store = LaunchDarkly::Integrations::Redis.new_feature_store(store_config) | ||
| opts[:feature_store] = store | ||
| when 'consul' | ||
| store_config[:url] = config[:persistentDataStore][:store][:url] | ||
| store = LaunchDarkly::Integrations::Consul.new_feature_store(store_config) | ||
| opts[:feature_store] = store | ||
| when 'dynamodb' | ||
| client = Aws::DynamoDB::Client.new( | ||
| region: 'us-east-1', | ||
| credentials: Aws::Credentials.new('dummy', 'dummy', 'dummy'), | ||
| endpoint: config[:persistentDataStore][:store][:dsn] | ||
| ) | ||
| store_config[:existing_client] = client | ||
| store = LaunchDarkly::Integrations::DynamoDB.new_feature_store('sdk-contract-tests', store_config) | ||
| opts[:feature_store] = store | ||
| end | ||
| # Configure persistent data store for legacy (non-dataSystem) configurations | ||
| if !data_system_config && config[:persistentDataStore] | ||
| store, _store_mode = build_persistent_store(config[:persistentDataStore]) | ||
| opts[:feature_store] = store | ||
jsonbailey marked this conversation as resolved.
Show resolved
Hide resolved
jsonbailey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| end | ||
|
|
||
| if config[:events] | ||
|
|
@@ -72,7 +116,7 @@ def initialize(log, config) | |
| opts[:diagnostic_opt_out] = !events[:enableDiagnostics] | ||
| opts[:all_attributes_private] = !!events[:allAttributesPrivate] | ||
| opts[:private_attributes] = events[:globalPrivateAttributes] | ||
| opts[:flush_interval] = (events[:flushIntervalMs] / 1_000) unless events[:flushIntervalMs].nil? | ||
| set_optional_time_prop(events, :flushIntervalMs, opts, :flush_interval) | ||
| opts[:omit_anonymous_contexts] = !!events[:omitAnonymousContexts] | ||
| opts[:compress_events] = !!events[:enableGzip] | ||
| else | ||
|
|
@@ -81,19 +125,14 @@ def initialize(log, config) | |
|
|
||
| if config[:bigSegments] | ||
| big_segments = config[:bigSegments] | ||
| big_config = { store: BigSegmentStoreFixture.new(big_segments[:callbackUri]) } | ||
|
|
||
| store = BigSegmentStoreFixture.new(config[:bigSegments][:callbackUri]) | ||
| context_cache_time = big_segments[:userCacheTimeMs].nil? ? nil : big_segments[:userCacheTimeMs] / 1_000 | ||
| status_poll_interval_ms = big_segments[:statusPollIntervalMs].nil? ? nil : big_segments[:statusPollIntervalMs] / 1_000 | ||
| stale_after_ms = big_segments[:staleAfterMs].nil? ? nil : big_segments[:staleAfterMs] / 1_000 | ||
|
|
||
| opts[:big_segments] = LaunchDarkly::BigSegmentsConfig.new( | ||
| store: store, | ||
| context_cache_size: big_segments[:userCacheSize], | ||
| context_cache_time: context_cache_time, | ||
| status_poll_interval: status_poll_interval_ms, | ||
| stale_after: stale_after_ms | ||
| ) | ||
| big_config[:context_cache_size] = big_segments[:userCacheSize] if big_segments[:userCacheSize] | ||
| set_optional_time_prop(big_segments, :userCacheTimeMs, big_config, :context_cache_time) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updating to use the new set_optional_time_prop method |
||
| set_optional_time_prop(big_segments, :statusPollIntervalMs, big_config, :status_poll_interval) | ||
| set_optional_time_prop(big_segments, :staleAfterMs, big_config, :stale_after) | ||
|
|
||
| opts[:big_segments] = LaunchDarkly::BigSegmentsConfig.new(**big_config) | ||
| end | ||
|
|
||
| if config[:tags] | ||
|
|
@@ -198,6 +237,50 @@ def context_comparison(params) | |
| context1 == context2 | ||
| end | ||
|
|
||
| def secure_mode_hash(params) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved non private methods above the private methods. |
||
| @client.secure_mode_hash(params[:context]) | ||
| end | ||
|
|
||
| def track(params) | ||
| @client.track(params[:eventKey], params[:context], params[:data], params[:metricValue]) | ||
| end | ||
|
|
||
| def identify(params) | ||
| @client.identify(params[:context]) | ||
| end | ||
|
|
||
| def flush_events | ||
| @client.flush | ||
| end | ||
|
|
||
| def get_big_segment_store_status | ||
| status = @client.big_segment_store_status_provider.status | ||
| { available: status.available, stale: status.stale } | ||
| end | ||
|
|
||
| def log | ||
| @log | ||
| end | ||
|
|
||
| def close | ||
| @client.close | ||
| @log.info("Test ended") | ||
| end | ||
|
|
||
| # | ||
| # Helper to convert millisecond time properties to seconds. | ||
| # Only sets the output if the input value is present. | ||
| # | ||
| # @param params_in [Hash] Input parameters hash | ||
| # @param name_in [Symbol] Key name in input hash (e.g., :pollIntervalMs) | ||
| # @param params_out [Hash] Output parameters hash | ||
| # @param name_out [Symbol] Key name in output hash (e.g., :poll_interval) | ||
| # | ||
| private def set_optional_time_prop(params_in, name_in, params_out, name_out) | ||
| value = params_in[name_in] | ||
| params_out[name_out] = value / 1_000.0 if value | ||
| end | ||
|
|
||
| private def build_context_from_params(params) | ||
| return build_single_context_from_attribute_definitions(params[:single]) unless params[:single].nil? | ||
|
|
||
|
|
@@ -230,33 +313,48 @@ def context_comparison(params) | |
| LaunchDarkly::LDContext.create(context) | ||
| end | ||
|
|
||
| def secure_mode_hash(params) | ||
| @client.secure_mode_hash(params[:context]) | ||
| end | ||
|
|
||
| def track(params) | ||
| @client.track(params[:eventKey], params[:context], params[:data], params[:metricValue]) | ||
| end | ||
|
|
||
| def identify(params) | ||
| @client.identify(params[:context]) | ||
| end | ||
|
|
||
| def flush_events | ||
| @client.flush | ||
| end | ||
|
|
||
| def get_big_segment_store_status | ||
| status = @client.big_segment_store_status_provider.status | ||
| { available: status.available, stale: status.stale } | ||
| end | ||
|
|
||
| def log | ||
| @log | ||
| end | ||
| # | ||
| # Builds a persistent data store from the contract test configuration. | ||
| # | ||
| # @param persistent_store_config [Hash] The persistentDataStore configuration | ||
| # @return [Array<Object, Symbol>] Returns [store, store_mode] | ||
| # | ||
| private def build_persistent_store(persistent_store_config) | ||
| store_config = {} | ||
| store_config[:prefix] = persistent_store_config[:store][:prefix] if persistent_store_config[:store][:prefix] | ||
|
|
||
| case persistent_store_config[:cache][:mode] | ||
| when 'off' | ||
| store_config[:expiration] = 0 | ||
| when 'infinite' | ||
| # NOTE: We don't actually support infinite cache mode, so we'll just set it to nil for now. This uses a default | ||
| # 15 second expiration time in the SDK, which is long enough to pass any test. | ||
| store_config[:expiration] = nil | ||
| when 'ttl' | ||
| store_config[:expiration] = persistent_store_config[:cache][:ttl] | ||
| end | ||
|
|
||
| def close | ||
| @client.close | ||
| @log.info("Test ended") | ||
| store = case persistent_store_config[:store][:type] | ||
| when 'redis' | ||
| store_config[:redis_url] = persistent_store_config[:store][:dsn] | ||
| LaunchDarkly::Integrations::Redis.new_feature_store(store_config) | ||
| when 'consul' | ||
| store_config[:url] = persistent_store_config[:store][:url] | ||
| LaunchDarkly::Integrations::Consul.new_feature_store(store_config) | ||
| when 'dynamodb' | ||
| client = Aws::DynamoDB::Client.new( | ||
| region: 'us-east-1', | ||
| credentials: Aws::Credentials.new('dummy', 'dummy', 'dummy'), | ||
| endpoint: persistent_store_config[:store][:dsn] | ||
| ) | ||
| store_config[:existing_client] = client | ||
| LaunchDarkly::Integrations::DynamoDB.new_feature_store('sdk-contract-tests', store_config) | ||
| end | ||
|
|
||
| store_mode = persistent_store_config[:mode] == 'read' ? | ||
| LaunchDarkly::Interfaces::DataSystem::DataStoreMode::READ_ONLY : | ||
| LaunchDarkly::Interfaces::DataSystem::DataStoreMode::READ_WRITE | ||
|
|
||
| [store, store_mode] | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,8 +17,8 @@ module DataSystem | |
| FDV2_POLLING_ENDPOINT = "/sdk/poll" | ||
| FDV1_POLLING_ENDPOINT = "/sdk/latest-all" | ||
|
|
||
| LD_ENVID_HEADER = "x-launchdarkly-env-id" | ||
| LD_FD_FALLBACK_HEADER = "x-launchdarkly-fd-fallback" | ||
| LD_ENVID_HEADER = "X-LD-EnvID" | ||
| LD_FD_FALLBACK_HEADER = "X-LD-FD-Fallback" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Header lookup fails due to case mismatchHigh Severity The |
||
|
|
||
| # | ||
| # Requester protocol for polling data source | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.