diff --git a/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb b/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb index d9d65523..85c91225 100644 --- a/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +++ b/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb @@ -28,7 +28,7 @@ def get(kind, key) items_of_kind = @items[kind] return nil if items_of_kind.nil? - item = items_of_kind[key] + item = items_of_kind[key.to_sym] return nil if item.nil? return nil if item[:deleted] diff --git a/lib/ldclient-rb/impl/data_store/store.rb b/lib/ldclient-rb/impl/data_store/store.rb index 2aa86daa..5fa7fcf4 100644 --- a/lib/ldclient-rb/impl/data_store/store.rb +++ b/lib/ldclient-rb/impl/data_store/store.rb @@ -284,7 +284,7 @@ def get_data_store_status_provider # Convert a list of Changes to the pre-existing format used by FeatureStore. # # @param changes [Array] List of changes - # @return [Hash{DataKind => Hash{String => Hash}}] Hash suitable for FeatureStore operations + # @return [Hash{DataKind => Hash{Symbol => Hash}}] Hash suitable for FeatureStore operations # private def changes_to_store_data(changes) all_data = { @@ -307,7 +307,7 @@ def get_data_store_status_provider # # Reset dependency tracker with new full data set. # - # @param all_data [Hash{DataKind => Hash{String => Hash}}] Hash of data kinds to items + # @param all_data [Hash{DataKind => Hash{Symbol => Hash}}] Hash of data kinds to items # @return [void] # private def reset_dependency_tracker(all_data) diff --git a/lib/ldclient-rb/impl/data_system/polling.rb b/lib/ldclient-rb/impl/data_system/polling.rb index 90b0abca..b1ead5ea 100644 --- a/lib/ldclient-rb/impl/data_system/polling.rb +++ b/lib/ldclient-rb/impl/data_system/polling.rb @@ -516,7 +516,7 @@ def self.fdv1_polling_payload_to_changeset(data) version = flag_or_segment[:version] return LaunchDarkly::Result.fail("Invalid format: #{key} does not have a version set") if version.nil? - builder.add_put(kind, key.to_s, version, flag_or_segment) + builder.add_put(kind, key, version, flag_or_segment) end end diff --git a/lib/ldclient-rb/impl/data_system/protocolv2.rb b/lib/ldclient-rb/impl/data_system/protocolv2.rb index d756a451..e8819fbd 100644 --- a/lib/ldclient-rb/impl/data_system/protocolv2.rb +++ b/lib/ldclient-rb/impl/data_system/protocolv2.rb @@ -22,13 +22,13 @@ class DeleteObject # @return [String] The object kind ({LaunchDarkly::Interfaces::DataSystem::ObjectKind}) attr_reader :kind - # @return [String] The key + # @return [Symbol] The key attr_reader :key # # @param version [Integer] The version # @param kind [String] The object kind ({LaunchDarkly::Interfaces::DataSystem::ObjectKind}) - # @param key [String] The key + # @param key [Symbol] The key # def initialize(version:, kind:, key:) @version = version @@ -72,7 +72,7 @@ def self.from_h(data) raise ArgumentError, "Missing required fields in DeleteObject" if version.nil? || kind.nil? || key.nil? - new(version: version, kind: kind, key: key) + new(version: version, kind: kind, key: key.to_sym) end end @@ -89,7 +89,7 @@ class PutObject # @return [String] The object kind ({LaunchDarkly::Interfaces::DataSystem::ObjectKind}) attr_reader :kind - # @return [String] The key + # @return [Symbol] The key attr_reader :key # @return [Hash] The object data @@ -98,7 +98,7 @@ class PutObject # # @param version [Integer] The version # @param kind [String] The object kind ({LaunchDarkly::Interfaces::DataSystem::ObjectKind}) - # @param key [String] The key + # @param key [Symbol] The key # @param object [Hash] The object data # def initialize(version:, kind:, key:, object:) @@ -146,7 +146,7 @@ def self.from_h(data) raise ArgumentError, "Missing required fields in PutObject" if version.nil? || kind.nil? || key.nil? || object_data.nil? - new(version: version, kind: kind, key: key, object: object_data) + new(version: version, kind: kind, key: key.to_sym, object: object_data) end end diff --git a/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb b/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb index cc40c8ef..95dbdf29 100644 --- a/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +++ b/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb @@ -192,9 +192,10 @@ def upsert_flag(flag_data) builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_CHANGES) # Add the updated flag + flag_key = flag_data[:key].to_sym builder.add_put( LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, - flag_data[:key], + flag_key, flag_data[:version] || 1, flag_data ) @@ -247,9 +248,10 @@ def upsert_segment(segment_data) builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_CHANGES) # Add the updated segment + segment_key = segment_data[:key].to_sym builder.add_put( LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT, - segment_data[:key], + segment_key, segment_data[:version] || 1, segment_data ) diff --git a/lib/ldclient-rb/impl/store_data_set_sorter.rb b/lib/ldclient-rb/impl/store_data_set_sorter.rb index 9ad15729..e5af482e 100644 --- a/lib/ldclient-rb/impl/store_data_set_sorter.rb +++ b/lib/ldclient-rb/impl/store_data_set_sorter.rb @@ -35,7 +35,7 @@ def self.sort_collection(kind, input) items_out = {} until remaining_items.empty? # pick a random item that hasn't been updated yet - key, item = remaining_items.first + _, item = remaining_items.first self.add_with_dependencies_first(item, dependency_fn, remaining_items, items_out) end items_out diff --git a/lib/ldclient-rb/integrations/test_data_v2.rb b/lib/ldclient-rb/integrations/test_data_v2.rb index 096480c4..1482b805 100644 --- a/lib/ldclient-rb/integrations/test_data_v2.rb +++ b/lib/ldclient-rb/integrations/test_data_v2.rb @@ -117,12 +117,13 @@ def update(flag_builder) instances_copy = [] new_flag = nil @lock.with_write_lock do - old_flag = @current_flags[flag_builder._key] + flag_key = flag_builder._key.to_sym + old_flag = @current_flags[flag_key] old_version = old_flag ? old_flag[:version] : 0 new_flag = flag_builder.build(old_version + 1) - @current_flags[flag_builder._key] = new_flag + @current_flags[flag_key] = new_flag @flag_builders[flag_builder._key] = flag_builder.clone # Create a copy of instances while holding the lock to avoid race conditions @@ -200,7 +201,7 @@ def use_preconfigured_segment(segment) else segment.as_json end - segment_key = segment_hash[:key] + segment_key = segment_hash[:key].to_sym old_segment = @current_segments[segment_key] old_version = old_segment ? old_segment[:version] : 0 diff --git a/lib/ldclient-rb/interfaces/data_system.rb b/lib/ldclient-rb/interfaces/data_system.rb index bd46538f..6447dc80 100644 --- a/lib/ldclient-rb/interfaces/data_system.rb +++ b/lib/ldclient-rb/interfaces/data_system.rb @@ -227,7 +227,7 @@ class Change # @return [String] The kind ({ObjectKind}) attr_reader :kind - # @return [String] The key + # @return [Symbol] The key attr_reader :key # @return [Integer] The version @@ -239,7 +239,7 @@ class Change # # @param action [String] The action type ({ChangeType}) # @param kind [String] The object kind ({ObjectKind}) - # @param key [String] The key + # @param key [Symbol] The key # @param version [Integer] The version # @param object [Hash, nil] The object data # @@ -546,7 +546,7 @@ def finish(selector) # Adds a new object to the changeset. # # @param kind [String] The object kind ({ObjectKind}) - # @param key [String] The key + # @param key [Symbol] The key # @param version [Integer] The version # @param obj [Hash] The object data # @return [void] @@ -565,7 +565,7 @@ def add_put(kind, key, version, obj) # Adds a deletion to the changeset. # # @param kind [String] The object kind ({ObjectKind}) - # @param key [String] The key + # @param key [Symbol] The key # @param version [Integer] The version # @return [void] # diff --git a/lib/ldclient-rb/interfaces/feature_store.rb b/lib/ldclient-rb/interfaces/feature_store.rb index a7a3c7d5..f3527b0b 100644 --- a/lib/ldclient-rb/interfaces/feature_store.rb +++ b/lib/ldclient-rb/interfaces/feature_store.rb @@ -36,7 +36,7 @@ module FeatureStore # the correct order), storing each item, and then delete any leftover items at the very end. # # @param all_data [Hash] a hash where each key is one of the data kind objects, and each - # value is in turn a hash of string keys to entities + # value is in turn a hash of symbol keys to entities # @return [void] # def init(all_data) @@ -46,7 +46,7 @@ def init(all_data) # Returns the entity to which the specified key is mapped, if any. # # @param kind [Object] the kind of entity to get - # @param key [String] the unique key of the entity to get + # @param key [String, Symbol] the unique key of the entity to get # @return [Hash] the entity; nil if the key was not found, or if the stored entity's # `:deleted` property was true # diff --git a/spec/impl/data_store/in_memory_feature_store_v2_spec.rb b/spec/impl/data_store/in_memory_feature_store_v2_spec.rb new file mode 100644 index 00000000..8b16cf19 --- /dev/null +++ b/spec/impl/data_store/in_memory_feature_store_v2_spec.rb @@ -0,0 +1,51 @@ +require "spec_helper" +require "ldclient-rb/impl/data_store/in_memory_feature_store" +require "ldclient-rb/impl/data_store" + +module LaunchDarkly + module Impl + module DataStore + describe InMemoryFeatureStoreV2 do + let(:logger) { double.as_null_object } + subject { InMemoryFeatureStoreV2.new(logger) } + + let(:flag_key) { "test-flag" } + let(:flag) do + { + key: flag_key, + version: 1, + on: true, + fallthrough: { variation: 0 }, + variations: [true, false], + } + end + + describe "#get with string/symbol key compatibility" do + before do + # Store items with symbol keys (as done by FDv2 protocol layer) + collections = { + FEATURES => { flag_key.to_sym => flag }, + } + subject.set_basis(collections) + end + + it "retrieves items with string keys (critical for variation calls)" do + result = subject.get(FEATURES, flag_key) + expect(result).to be_a(LaunchDarkly::Impl::Model::FeatureFlag) + expect(result.key).to eq(flag_key) + end + + it "retrieves items with symbol keys" do + result = subject.get(FEATURES, flag_key.to_sym) + expect(result).to be_a(LaunchDarkly::Impl::Model::FeatureFlag) + expect(result.key).to eq(flag_key) + end + + it "returns nil for non-existent keys" do + expect(subject.get(FEATURES, "nonexistent")).to be_nil + end + end + end + end + end +end diff --git a/spec/impl/data_system/fdv2_datasystem_spec.rb b/spec/impl/data_system/fdv2_datasystem_spec.rb index bf7177b0..c94cab9f 100644 --- a/spec/impl/data_system/fdv2_datasystem_spec.rb +++ b/spec/impl/data_system/fdv2_datasystem_spec.rb @@ -14,14 +14,14 @@ module DataSystem describe "two-phase initialization" do it "initializes from initializer then syncs from synchronizer" do td_initializer = LaunchDarkly::Integrations::TestDataV2.data_source - td_initializer.update(td_initializer.flag("feature-flag").on(true)) + td_initializer.update(td_initializer.flag("flagkey").on(true)) td_synchronizer = LaunchDarkly::Integrations::TestDataV2.data_source # Set this to true, and then to false to ensure the version number exceeded # the initializer version number. Otherwise, they start as the same version # and the latest value is ignored. - td_synchronizer.update(td_synchronizer.flag("feature-flag").on(true)) - td_synchronizer.update(td_synchronizer.flag("feature-flag").on(false)) + td_synchronizer.update(td_synchronizer.flag("flagkey").on(true)) + td_synchronizer.update(td_synchronizer.flag("flagkey").on(false)) data_system_config = LaunchDarkly::DataSystem::ConfigBuilder.new .initializers([td_initializer.method(:build_initializer)]) @@ -50,13 +50,13 @@ module DataSystem expect(ready_event.wait(2)).to be true expect(initialized.wait(1)).to be true - td_synchronizer.update(td_synchronizer.flag("feature-flag").on(true)) + td_synchronizer.update(td_synchronizer.flag("flagkey").on(true)) expect(modified.wait(1)).to be true expect(changes.length).to eq(3) - expect(changes[0].key).to eq("feature-flag") - expect(changes[1].key).to eq("feature-flag") - expect(changes[2].key).to eq("feature-flag") + expect(changes[0].key).to eq(:flagkey) + expect(changes[1].key).to eq(:flagkey) + expect(changes[2].key).to eq(:flagkey) fdv2.stop end @@ -88,7 +88,7 @@ module DataSystem fdv2.stop - td.update(td.flag("feature-flag").on(false)) + td.update(td.flag("flagkey").on(false)) expect(changed.wait(1)).to be_falsey, "Flag change listener was erroneously called" expect(changes.length).to eq(0) end @@ -123,7 +123,7 @@ module DataSystem allow(mock_primary).to receive(:sync) td = LaunchDarkly::Integrations::TestDataV2.data_source - td.update(td.flag("feature-flag").on(true)) + td.update(td.flag("flagkey").on(true)) data_system_config = LaunchDarkly::DataSystem::ConfigBuilder.new .initializers([td.method(:build_initializer)]) @@ -150,12 +150,12 @@ module DataSystem ready_event = fdv2.start expect(ready_event.wait(2)).to be true - td.update(td.flag("feature-flag").on(false)) + td.update(td.flag("flagkey").on(false)) expect(changed.wait(2)).to be true expect(changes.length).to eq(2) - expect(changes[0].key).to eq("feature-flag") - expect(changes[1].key).to eq("feature-flag") + expect(changes[0].key).to eq(:flagkey) + expect(changes[1].key).to eq(:flagkey) fdv2.stop end @@ -176,7 +176,7 @@ module DataSystem allow(mock_secondary).to receive(:sync) td = LaunchDarkly::Integrations::TestDataV2.data_source - td.update(td.flag("feature-flag").on(true)) + td.update(td.flag("flagkey").on(true)) data_system_config = LaunchDarkly::DataSystem::ConfigBuilder.new .initializers([td.method(:build_initializer)]) @@ -221,7 +221,7 @@ module DataSystem # Create FDv1 fallback data source with actual data td_fdv1 = LaunchDarkly::Integrations::TestDataV2.data_source - td_fdv1.update(td_fdv1.flag("fdv1-flag").on(true)) + td_fdv1.update(td_fdv1.flag("fdv1flag").on(true)) data_system_config = LaunchDarkly::DataSystem::ConfigBuilder.new .initializers(nil) @@ -245,12 +245,12 @@ module DataSystem expect(ready_event.wait(1)).to be true # Update flag in FDv1 data source to verify it's being used - td_fdv1.update(td_fdv1.flag("fdv1-flag").on(false)) + td_fdv1.update(td_fdv1.flag("fdv1flag").on(false)) expect(changed.wait(10)).to be true # Verify we got flag changes from FDv1 expect(changes.length).to be > 0 - expect(changes.any? { |change| change.key == "fdv1-flag" }).to be true + expect(changes.any? { |change| change.key == :fdv1flag }).to be true fdv2.stop end @@ -270,7 +270,7 @@ module DataSystem # Create FDv1 fallback data source td_fdv1 = LaunchDarkly::Integrations::TestDataV2.data_source - td_fdv1.update(td_fdv1.flag("fdv1-fallback-flag").on(true)) + td_fdv1.update(td_fdv1.flag("fdv1fallbackflag").on(true)) data_system_config = LaunchDarkly::DataSystem::ConfigBuilder.new .initializers(nil) @@ -300,12 +300,12 @@ module DataSystem changed = Concurrent::Event.new # Reset for second change # Trigger a flag update in FDv1 - td_fdv1.update(td_fdv1.flag("fdv1-fallback-flag").on(false)) + td_fdv1.update(td_fdv1.flag("fdv1fallbackflag").on(false)) expect(changed.wait(2)).to be true # Verify FDv1 is active and we got both changes expect(changes.length).to eq(2) - expect(changes.all? { |change| change.key == "fdv1-fallback-flag" }).to be true + expect(changes.all? { |change| change.key == :fdv1fallbackflag }).to be true fdv2.stop end @@ -315,7 +315,7 @@ module DataSystem it "falls back to FDv1 and replaces initialized data" do # Initialize with some data td_initializer = LaunchDarkly::Integrations::TestDataV2.data_source - td_initializer.update(td_initializer.flag("initial-flag").on(true)) + td_initializer.update(td_initializer.flag("initialflag").on(true)) # Create mock primary that signals fallback mock_primary = double("primary_synchronizer") @@ -330,7 +330,7 @@ module DataSystem # Create FDv1 fallback with different data td_fdv1 = LaunchDarkly::Integrations::TestDataV2.data_source - td_fdv1.update(td_fdv1.flag("fdv1-replacement-flag").on(true)) + td_fdv1.update(td_fdv1.flag("fdv1replacementflag").on(true)) data_system_config = LaunchDarkly::DataSystem::ConfigBuilder.new .initializers([td_initializer.method(:build_initializer)]) @@ -356,8 +356,8 @@ module DataSystem # Verify we got changes for both flags flag_keys = changes.map { |change| change.key } - expect(flag_keys).to include("initial-flag") - expect(flag_keys).to include("fdv1-replacement-flag") + expect(flag_keys).to include(:initialflag) + expect(flag_keys).to include(:fdv1replacementflag) fdv2.stop end @@ -429,7 +429,7 @@ module DataSystem # Create FDv1 fallback td_fdv1 = LaunchDarkly::Integrations::TestDataV2.data_source - td_fdv1.update(td_fdv1.flag("fdv1-flag").on(true)) + td_fdv1.update(td_fdv1.flag("fdv1flag").on(true)) data_system_config = LaunchDarkly::DataSystem::ConfigBuilder.new .initializers(nil) @@ -450,7 +450,7 @@ module DataSystem # Verify FDv1 is serving data store = fdv2.store - flag = store.get(LaunchDarkly::Impl::DataStore::FEATURES, "fdv1-flag") + flag = store.get(LaunchDarkly::Impl::DataStore::FEATURES, :fdv1flag) expect(flag).not_to be_nil fdv2.stop diff --git a/spec/impl/data_system/fdv2_persistence_spec.rb b/spec/impl/data_system/fdv2_persistence_spec.rb new file mode 100644 index 00000000..71e58122 --- /dev/null +++ b/spec/impl/data_system/fdv2_persistence_spec.rb @@ -0,0 +1,304 @@ +# frozen_string_literal: true + +require "spec_helper" +require "ldclient-rb/impl/data_system/fdv2" +require "ldclient-rb/integrations/test_data_v2" +require "ldclient-rb/data_system" +require "ldclient-rb/impl/data_system" +require "ldclient-rb/impl/data_store" + +module LaunchDarkly + module Impl + module DataSystem + describe "FDv2 Persistent Store Recovery" do + let(:sdk_key) { "test-sdk-key" } + let(:test_logger) do + logger = ::Logger.new($stdout) + logger.level = ::Logger::DEBUG + logger + end + let(:config) do + logger = ::Logger.new(STDOUT) + logger.level = ::Logger::DEBUG + LaunchDarkly::Config.new(logger: logger) + end + + # Stub feature store for testing + class StubFeatureStore + include LaunchDarkly::Interfaces::FeatureStore + + attr_reader :init_called_count, :upsert_calls, :data + + def initialize(initial_data = nil) + @data = { + LaunchDarkly::Impl::DataStore::FEATURES => {}, + LaunchDarkly::Impl::DataStore::SEGMENTS => {}, + } + @initialized = false + @available = true + @monitoring_enabled = true # Enable monitoring by default + @init_called_count = 0 + @upsert_calls = [] + + init(initial_data) if initial_data + end + + def init(all_data) + @init_called_count += 1 + if all_data + @data[LaunchDarkly::Impl::DataStore::FEATURES] = (all_data[LaunchDarkly::Impl::DataStore::FEATURES] || {}).dup + @data[LaunchDarkly::Impl::DataStore::SEGMENTS] = (all_data[LaunchDarkly::Impl::DataStore::SEGMENTS] || {}).dup + end + @initialized = true + end + + def get(kind, key) + item = @data[kind][key.to_sym] || @data[kind][key.to_s] + item && !item[:deleted] ? item : nil + end + + def all(kind) + @data[kind].reject { |_k, v| v[:deleted] } + end + + def delete(kind, key, version) + existing = @data[kind][key] + if !existing || existing[:version] < version + @data[kind][key] = { key: key, version: version, deleted: true } + end + end + + def upsert(kind, item) + @upsert_calls << [kind, item[:key], item[:version]] + key = item[:key] + existing = @data[kind][key] + if !existing || existing[:version] < item[:version] + @data[kind][key] = item + end + end + + def initialized? + @initialized + end + + def available? + @available + end + + def monitoring_enabled? + @monitoring_enabled + end + + def stop + # No-op + end + + # Test helpers + def set_available(available) + @available = available + end + + def enable_monitoring + @monitoring_enabled = true + end + + def reset_operation_tracking + @init_called_count = 0 + @upsert_calls = [] + end + + def get_data_snapshot + { + LaunchDarkly::Impl::DataStore::FEATURES => @data[LaunchDarkly::Impl::DataStore::FEATURES].dup, + LaunchDarkly::Impl::DataStore::SEGMENTS => @data[LaunchDarkly::Impl::DataStore::SEGMENTS].dup, + } + end + end + + it "flushes in-memory store to persistent store when it recovers from outage with stale data" do + persistent_store = StubFeatureStore.new + + # Create and populate synchronizer BEFORE building config + td_synchronizer = LaunchDarkly::Integrations::TestDataV2.data_source + td_synchronizer.update(td_synchronizer.flag("flagkey").on(true)) + + data_system_config = LaunchDarkly::DataSystem::ConfigBuilder.new + .initializers(nil) + .synchronizers(td_synchronizer.method(:build_synchronizer)) + .data_store(persistent_store, :read_write) + .build + + fdv2 = FDv2.new(sdk_key, config, data_system_config) + ready_event = fdv2.start + + expect(ready_event.wait(3)).to be true + + # Verify data from synchronizer is in the persistent store + snapshot = persistent_store.get_data_snapshot + expect(snapshot[LaunchDarkly::Impl::DataStore::FEATURES]).to have_key(:flagkey) + expect(snapshot[LaunchDarkly::Impl::DataStore::FEATURES][:flagkey][:on]).to be true + + # Reset tracking to isolate recovery behavior + persistent_store.reset_operation_tracking + + # Simulate a new flag being added while store is "offline" + flag_changed = Concurrent::Event.new + changes = [] + listener = Object.new + listener.define_singleton_method(:update) do |flag_change| + changes << flag_change + flag_changed.set if flag_change.key == :newflag + end + fdv2.flag_change_broadcaster.add_listener(listener) + + td_synchronizer.update(td_synchronizer.flag("newflag").on(false)) + + # Wait for the flag to propagate + expect(flag_changed.wait(2)).to be true + + # Now simulate the persistent store coming back online with stale data + # by triggering the recovery callback directly + stale_status = LaunchDarkly::Interfaces::DataStore::Status.new(true, true) + fdv2.send(:persistent_store_outage_recovery, stale_status) + + # Verify that init was called on the persistent store (flushing in-memory data) + expect(persistent_store.init_called_count).to be > 0 + + # Verify both flags are now in the persistent store + snapshot = persistent_store.get_data_snapshot + expect(snapshot[LaunchDarkly::Impl::DataStore::FEATURES]).to have_key(:flagkey) + expect(snapshot[LaunchDarkly::Impl::DataStore::FEATURES]).to have_key(:newflag) + + fdv2.stop + end + + it "does not flush when store comes back online without stale data" do + persistent_store = StubFeatureStore.new + + td_synchronizer = LaunchDarkly::Integrations::TestDataV2.data_source + td_synchronizer.update(td_synchronizer.flag("flagkey").on(true)) + + data_system_config = LaunchDarkly::DataSystem::ConfigBuilder.new + .initializers(nil) + .synchronizers(td_synchronizer.method(:build_synchronizer)) + .data_store(persistent_store, :read_write) + .build + + fdv2 = FDv2.new(sdk_key, config, data_system_config) + ready_event = fdv2.start + + expect(ready_event.wait(2)).to be true + + # Reset tracking + persistent_store.reset_operation_tracking + + # Simulate store coming back online but NOT stale (data is fresh) + fresh_status = LaunchDarkly::Interfaces::DataStore::Status.new(true, false) + fdv2.send(:persistent_store_outage_recovery, fresh_status) + + # Verify that init was NOT called (no flush needed) + expect(persistent_store.init_called_count).to eq(0) + + fdv2.stop + end + + it "does not flush when store is unavailable" do + persistent_store = StubFeatureStore.new + + td_synchronizer = LaunchDarkly::Integrations::TestDataV2.data_source + td_synchronizer.update(td_synchronizer.flag("flagkey").on(true)) + + data_system_config = LaunchDarkly::DataSystem::ConfigBuilder.new + .initializers(nil) + .synchronizers(td_synchronizer.method(:build_synchronizer)) + .data_store(persistent_store, :read_write) + .build + + fdv2 = FDv2.new(sdk_key, config, data_system_config) + ready_event = fdv2.start + + expect(ready_event.wait(2)).to be true + + # Reset tracking + persistent_store.reset_operation_tracking + + # Simulate store being unavailable (even if marked as stale) + unavailable_status = LaunchDarkly::Interfaces::DataStore::Status.new(false, true) + fdv2.send(:persistent_store_outage_recovery, unavailable_status) + + # Verify that init was NOT called (store is not available) + expect(persistent_store.init_called_count).to eq(0) + + fdv2.stop + end + + it "works in READ_WRITE mode with persistent store" do + persistent_store = StubFeatureStore.new + + td_synchronizer = LaunchDarkly::Integrations::TestDataV2.data_source + td_synchronizer.update(td_synchronizer.flag("flagkey").on(true)) + + data_system_config = LaunchDarkly::DataSystem::ConfigBuilder.new + .initializers(nil) + .synchronizers(td_synchronizer.method(:build_synchronizer)) + .data_store(persistent_store, :read_write) + .build + + fdv2 = FDv2.new(sdk_key, config, data_system_config) + ready_event = fdv2.start + + expect(ready_event.wait(2)).to be true + + # Verify data was written to persistent store + expect(persistent_store.init_called_count).to be >= 1 + + # Verify the flag is in the persistent store + snapshot = persistent_store.get_data_snapshot + expect(snapshot[LaunchDarkly::Impl::DataStore::FEATURES]).to have_key(:flagkey) + + fdv2.stop + end + + it "works in READ_ONLY mode with persistent store" do + # Pre-populate persistent store + initial_data = { + LaunchDarkly::Impl::DataStore::FEATURES => { + :existingflag => { + key: "existingflag", + version: 1, + on: true, + variations: [true, false], + fallthrough: { variation: 0 }, + }, + }, + LaunchDarkly::Impl::DataStore::SEGMENTS => {}, + } + + persistent_store = StubFeatureStore.new(initial_data) + persistent_store.reset_operation_tracking + + # Create synchronizer with new data + td_synchronizer = LaunchDarkly::Integrations::TestDataV2.data_source + td_synchronizer.update(td_synchronizer.flag("newflag").on(true)) + + data_system_config = LaunchDarkly::DataSystem::ConfigBuilder.new + .initializers(nil) + .synchronizers(td_synchronizer.method(:build_synchronizer)) + .data_store(persistent_store, :read_only) + .build + + fdv2 = FDv2.new(sdk_key, config, data_system_config) + ready_event = fdv2.start + + expect(ready_event.wait(2)).to be true + + # In READ_ONLY mode, no writes should happen to persistent store + expect(persistent_store.init_called_count).to eq(0) + expect(persistent_store.upsert_calls).to be_empty + + fdv2.stop + end + end + end + end +end diff --git a/spec/impl/data_system/polling_payload_parsing_spec.rb b/spec/impl/data_system/polling_payload_parsing_spec.rb index 93688f5b..877ad5ce 100644 --- a/spec/impl/data_system/polling_payload_parsing_spec.rb +++ b/spec/impl/data_system/polling_payload_parsing_spec.rb @@ -63,7 +63,7 @@ module DataSystem end it "processes put object" do - payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "put-object","data": {"key":"sample-feature","kind":"flag","version":461,"object":{"key":"sample-feature","on":false,"prerequisites":[],"targets":[],"contextTargets":[],"rules":[],"fallthrough":{"variation":0},"offVariation":1,"variations":[true,false],"clientSideAvailability":{"usingMobileKey":false,"usingEnvironmentId":false},"clientSide":false,"salt":"9945e63a79a44787805b79728fee1926","trackEvents":false,"trackEventsFallthrough":false,"debugEventsUntilDate":null,"version":112,"deleted":false}}},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}}]}' # rubocop:disable Layout/LineLength + payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "put-object","data": {"key":"sampleflag","kind":"flag","version":461,"object":{"key":"sampleflag","on":false,"prerequisites":[],"targets":[],"contextTargets":[],"rules":[],"fallthrough":{"variation":0},"offVariation":1,"variations":[true,false],"clientSideAvailability":{"usingMobileKey":false,"usingEnvironmentId":false},"clientSide":false,"salt":"9945e63a79a44787805b79728fee1926","trackEvents":false,"trackEventsFallthrough":false,"debugEventsUntilDate":null,"version":112,"deleted":false}}},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}}]}' # rubocop:disable Layout/LineLength result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true)) expect(result).not_to be_nil @@ -72,7 +72,7 @@ module DataSystem expect(result.value.changes[0].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::PUT) expect(result.value.changes[0].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG) - expect(result.value.changes[0].key).to eq("sample-feature") + expect(result.value.changes[0].key).to eq(:sampleflag) expect(result.value.changes[0].version).to eq(461) expect(result.value.changes[0].object).to be_a(Hash) @@ -82,7 +82,7 @@ module DataSystem end it "processes delete object" do - payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "delete-object","data": {"key":"sample-feature","kind":"flag","version":461}},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}}]}' # rubocop:disable Layout/LineLength + payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "delete-object","data": {"key":"sampleflag","kind":"flag","version":461}},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}}]}' # rubocop:disable Layout/LineLength result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true)) expect(result).not_to be_nil @@ -91,7 +91,7 @@ module DataSystem expect(result.value.changes[0].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::DELETE) expect(result.value.changes[0].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG) - expect(result.value.changes[0].key).to eq("sample-feature") + expect(result.value.changes[0].key).to eq(:sampleflag) expect(result.value.changes[0].version).to eq(461) expect(result.value.changes[0].object).to be_nil @@ -119,13 +119,13 @@ module DataSystem end it "fails if starts with transferred" do - payload_str = '{"events":[ {"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}},{"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "put-object","data": {"key":"sample-feature","kind":"flag","version":461,"object":{"key":"sample-feature","on":false,"prerequisites":[],"targets":[],"contextTargets":[],"rules":[],"fallthrough":{"variation":0},"offVariation":1,"variations":[true,false],"clientSideAvailability":{"usingMobileKey":false,"usingEnvironmentId":false},"clientSide":false,"salt":"9945e63a79a44787805b79728fee1926","trackEvents":false,"trackEventsFallthrough":false,"debugEventsUntilDate":null,"version":112,"deleted":false}}}]}' # rubocop:disable Layout/LineLength + payload_str = '{"events":[ {"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}},{"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "put-object","data": {"key":"sampleflag","kind":"flag","version":461,"object":{"key":"sampleflag","on":false,"prerequisites":[],"targets":[],"contextTargets":[],"rules":[],"fallthrough":{"variation":0},"offVariation":1,"variations":[true,false],"clientSideAvailability":{"usingMobileKey":false,"usingEnvironmentId":false},"clientSide":false,"salt":"9945e63a79a44787805b79728fee1926","trackEvents":false,"trackEventsFallthrough":false,"debugEventsUntilDate":null,"version":112,"deleted":false}}}]}' # rubocop:disable Layout/LineLength result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true)) expect(result.success?).to eq(false) end it "fails if starts with put" do - payload_str = '{"events":[ {"event": "put-object","data": {"key":"sample-feature","kind":"flag","version":461,"object":{"key":"sample-feature","on":false,"prerequisites":[],"targets":[],"contextTargets":[],"rules":[],"fallthrough":{"variation":0},"offVariation":1,"variations":[true,false],"clientSideAvailability":{"usingMobileKey":false,"usingEnvironmentId":false},"clientSide":false,"salt":"9945e63a79a44787805b79728fee1926","trackEvents":false,"trackEventsFallthrough":false,"debugEventsUntilDate":null,"version":112,"deleted":false}}},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}},{"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}}]}' # rubocop:disable Layout/LineLength + payload_str = '{"events":[ {"event": "put-object","data": {"key":"sampleflag","kind":"flag","version":461,"object":{"key":"sampleflag","on":false,"prerequisites":[],"targets":[],"contextTargets":[],"rules":[],"fallthrough":{"variation":0},"offVariation":1,"variations":[true,false],"clientSideAvailability":{"usingMobileKey":false,"usingEnvironmentId":false},"clientSide":false,"salt":"9945e63a79a44787805b79728fee1926","trackEvents":false,"trackEventsFallthrough":false,"debugEventsUntilDate":null,"version":112,"deleted":false}}},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}},{"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}}]}' # rubocop:disable Layout/LineLength result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true)) expect(result.success?).to eq(false) end @@ -149,8 +149,8 @@ module DataSystem it "handles single flag" do data = { flags: { - "test-flag" => { - key: "test-flag", + testflag: { + key: "testflag", version: 1, on: true, variations: [true, false], @@ -167,16 +167,16 @@ module DataSystem change = result.value.changes[0] expect(change.action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::PUT) expect(change.kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG) - expect(change.key).to eq("test-flag") + expect(change.key).to eq(:testflag) expect(change.version).to eq(1) end it "handles multiple flags" do data = { flags: { - "flag-1" => { key: "flag-1", version: 1, on: true }, - "flag-2" => { key: "flag-2", version: 2, on: false }, - "flag-3" => { key: "flag-3", version: 3, on: true }, + 'flag-1': { key: "flag-1", version: 1, on: true }, + 'flag-2': { key: "flag-2", version: 2, on: false }, + 'flag-3': { key: "flag-3", version: 3, on: true }, }, segments: {}, } @@ -185,15 +185,15 @@ module DataSystem expect(result.value.changes.length).to eq(3) flag_keys = result.value.changes.map(&:key).to_set - expect(flag_keys).to eq(Set["flag-1", "flag-2", "flag-3"]) + expect(flag_keys).to eq(Set[:"flag-1", :"flag-2", :"flag-3"]) end it "handles single segment" do data = { flags: {}, segments: { - "test-segment" => { - key: "test-segment", + testsegment: { + key: "testsegment", version: 5, included: ["user1", "user2"], }, @@ -206,19 +206,19 @@ module DataSystem change = result.value.changes[0] expect(change.action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::PUT) expect(change.kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT) - expect(change.key).to eq("test-segment") + expect(change.key).to eq(:testsegment) expect(change.version).to eq(5) end it "handles flags and segments" do data = { flags: { - "flag-1" => { key: "flag-1", version: 1, on: true }, - "flag-2" => { key: "flag-2", version: 2, on: false }, + 'flag-1': { key: "flag-1", version: 1, on: true }, + 'flag-2': { key: "flag-2", version: 2, on: false }, }, segments: { - "segment-1" => { key: "segment-1", version: 10 }, - "segment-2" => { key: "segment-2", version: 20 }, + 'segment-1': { key: "segment-1", version: 10 }, + 'segment-2': { key: "segment-2", version: 20 }, }, } result = LaunchDarkly::Impl::DataSystem.fdv1_polling_payload_to_changeset(data) @@ -290,27 +290,27 @@ module DataSystem it "works with only flags, no segments key" do data = { flags: { - "test-flag" => { key: "test-flag", version: 1, on: true }, + testflag: { key: "testflag", version: 1, on: true }, }, } result = LaunchDarkly::Impl::DataSystem.fdv1_polling_payload_to_changeset(data) expect(result).not_to be_nil expect(result.value.changes.length).to eq(1) - expect(result.value.changes[0].key).to eq("test-flag") + expect(result.value.changes[0].key).to eq(:testflag) end it "works with only segments, no flags key" do data = { segments: { - "test-segment" => { key: "test-segment", version: 1 }, + testsegment: { key: "testsegment", version: 1 }, }, } result = LaunchDarkly::Impl::DataSystem.fdv1_polling_payload_to_changeset(data) expect(result).not_to be_nil expect(result.value.changes.length).to eq(1) - expect(result.value.changes[0].key).to eq("test-segment") + expect(result.value.changes[0].key).to eq(:testsegment) end end end diff --git a/spec/impl/data_system/polling_synchronizer_spec.rb b/spec/impl/data_system/polling_synchronizer_spec.rb index 10b1783f..c4fca3c8 100644 --- a/spec/impl/data_system/polling_synchronizer_spec.rb +++ b/spec/impl/data_system/polling_synchronizer_spec.rb @@ -126,9 +126,9 @@ def selector builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL) builder.add_put( LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, - "flag-key", + :flagkey, 100, - { key: "flag-key" } + { key: "flagkey" } ) change_set = builder.finish(LaunchDarkly::Interfaces::DataSystem::Selector.new(state: "p:SOMETHING:300", version: 300)) headers = {} @@ -158,8 +158,8 @@ def selector expect(valid.change_set.changes.length).to eq(1) expect(valid.change_set.changes[0].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::PUT) expect(valid.change_set.changes[0].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG) - expect(valid.change_set.changes[0].key).to eq("flag-key") - expect(valid.change_set.changes[0].object).to eq({ key: "flag-key" }) + expect(valid.change_set.changes[0].key).to eq(:flagkey) + expect(valid.change_set.changes[0].object).to eq({ key: "flagkey" }) expect(valid.change_set.changes[0].version).to eq(100) expect(valid.change_set.selector).not_to be_nil expect(valid.change_set.selector.version).to eq(300) @@ -170,7 +170,7 @@ def selector it "handles delete objects" do builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL) - builder.add_delete(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, "flag-key", 101) + builder.add_delete(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, :flagkey, 101) change_set = builder.finish(LaunchDarkly::Interfaces::DataSystem::Selector.new(state: "p:SOMETHING:300", version: 300)) headers = {} polling_result = LaunchDarkly::Result.success([change_set, headers]) @@ -199,7 +199,7 @@ def selector expect(valid.change_set.changes.length).to eq(1) expect(valid.change_set.changes[0].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::DELETE) expect(valid.change_set.changes[0].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG) - expect(valid.change_set.changes[0].key).to eq("flag-key") + expect(valid.change_set.changes[0].key).to eq(:flagkey) expect(valid.change_set.changes[0].version).to eq(101) expect(valid.change_set.selector).not_to be_nil expect(valid.change_set.selector.version).to eq(300) @@ -210,7 +210,7 @@ def selector it "generic error interrupts and recovers" do builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL) - builder.add_delete(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, "flag-key", 101) + builder.add_delete(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, "flagkey", 101) change_set = builder.finish(LaunchDarkly::Interfaces::DataSystem::Selector.new(state: "p:SOMETHING:300", version: 300)) headers = {} polling_result = LaunchDarkly::Result.success([change_set, headers]) @@ -256,7 +256,7 @@ def selector it "recoverable error continues" do builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL) - builder.add_delete(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, "flag-key", 101) + builder.add_delete(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, "flagkey", 101) change_set = builder.finish(LaunchDarkly::Interfaces::DataSystem::Selector.new(state: "p:SOMETHING:300", version: 300)) headers = {} polling_result = LaunchDarkly::Result.success([change_set, headers]) @@ -370,9 +370,9 @@ def selector builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL) builder.add_put( LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, - "flag-key", + :flagkey, 100, - { key: "flag-key" } + { key: "flagkey" } ) change_set = builder.finish(LaunchDarkly::Interfaces::DataSystem::Selector.new(state: "p:SOMETHING:300", version: 300)) headers = { @@ -407,7 +407,7 @@ def selector it "captures envid from error headers recoverable" do builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL) - builder.add_delete(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, "flag-key", 101) + builder.add_delete(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, "flagkey", 101) change_set = builder.finish(LaunchDarkly::Interfaces::DataSystem::Selector.new(state: "p:SOMETHING:300", version: 300)) headers_success = { LD_ENVID_HEADER => 'test-env-success' } polling_result = LaunchDarkly::Result.success([change_set, headers_success]) diff --git a/spec/impl/data_system/streaming_synchronizer_spec.rb b/spec/impl/data_system/streaming_synchronizer_spec.rb index e695d48b..06fdacb0 100644 --- a/spec/impl/data_system/streaming_synchronizer_spec.rb +++ b/spec/impl/data_system/streaming_synchronizer_spec.rb @@ -124,8 +124,8 @@ def initialize(type, data = nil) put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.new( version: 100, kind: LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, - key: "flag-key", - object: { key: "flag-key" } + key: "flagkey", + object: { key: "flagkey" } ) selector = LaunchDarkly::Interfaces::DataSystem::Selector.new(state: "p:SOMETHING:300", version: 300) @@ -159,8 +159,8 @@ def initialize(type, data = nil) expect(update.change_set.changes.length).to eq(1) expect(update.change_set.changes[0].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::PUT) expect(update.change_set.changes[0].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG) - expect(update.change_set.changes[0].key).to eq("flag-key") - expect(update.change_set.changes[0].object).to eq({ key: "flag-key" }) + expect(update.change_set.changes[0].key).to eq(:flagkey) + expect(update.change_set.changes[0].object).to eq({ key: "flagkey" }) expect(update.change_set.changes[0].version).to eq(100) end @@ -176,7 +176,7 @@ def initialize(type, data = nil) delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.new( version: 101, kind: LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, - key: "flag-key" + key: "flagkey" ) selector = LaunchDarkly::Interfaces::DataSystem::Selector.new(state: "p:SOMETHING:300", version: 300) @@ -210,7 +210,7 @@ def initialize(type, data = nil) expect(update.change_set.changes.length).to eq(1) expect(update.change_set.changes[0].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::DELETE) expect(update.change_set.changes[0].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG) - expect(update.change_set.changes[0].key).to eq("flag-key") + expect(update.change_set.changes[0].key).to eq(:flagkey) expect(update.change_set.changes[0].version).to eq(101) end @@ -270,8 +270,8 @@ def initialize(type, data = nil) put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.new( version: 100, kind: LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, - key: "flag-key", - object: { key: "flag-key" } + key: "flagkey", + object: { key: "flagkey" } ) error = LaunchDarkly::Impl::DataSystem::ProtocolV2::Error.new( payload_id: "p:SOMETHING:300", @@ -280,7 +280,7 @@ def initialize(type, data = nil) delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.new( version: 101, kind: LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, - key: "flag-key" + key: "flagkey" ) selector = LaunchDarkly::Interfaces::DataSystem::Selector.new(state: "p:SOMETHING:300", version: 300) diff --git a/spec/integrations/test_data_v2_spec.rb b/spec/integrations/test_data_v2_spec.rb index a15c7d86..d157df40 100644 --- a/spec/integrations/test_data_v2_spec.rb +++ b/spec/integrations/test_data_v2_spec.rb @@ -14,48 +14,48 @@ module Integrations it 'stores flags' do td = TestDataV2.data_source - td.update(td.flag('my-flag').variation_for_all(true)) + td.update(td.flag('myflag').variation_for_all(true)) init_data = td.make_init_data - expect(init_data[:flags].keys).to include('my-flag') - expect(init_data[:flags]['my-flag'][:key]).to eq('my-flag') + expect(init_data[:flags].keys).to include(:myflag) + expect(init_data[:flags][:myflag][:key]).to eq('myflag') end it 'stores preconfigured segments' do td = TestDataV2.data_source - td.use_preconfigured_segment({ key: 'my-segment', version: 100, included: ['user1'] }) + td.use_preconfigured_segment({ key: 'mysegment', version: 100, included: ['user1'] }) init_data = td.make_init_data - expect(init_data[:segments].keys).to include('my-segment') - expect(init_data[:segments]['my-segment'][:key]).to eq('my-segment') - expect(init_data[:segments]['my-segment'][:version]).to eq(1) - expect(init_data[:segments]['my-segment'][:included]).to eq(['user1']) + expect(init_data[:segments].keys).to include(:mysegment) + expect(init_data[:segments][:mysegment][:key]).to eq('mysegment') + expect(init_data[:segments][:mysegment][:version]).to eq(1) + expect(init_data[:segments][:mysegment][:included]).to eq(['user1']) end it 'handles segments with string-keyed hashes' do td = TestDataV2.data_source # Use string keys instead of symbol keys - td.use_preconfigured_segment({ 'key' => 'my-segment', 'version' => 100, 'included' => ['user1'], 'excluded' => ['user2'] }) + td.use_preconfigured_segment({ 'key' => 'mysegment', 'version' => 100, 'included' => ['user1'], 'excluded' => ['user2'] }) init_data = td.make_init_data - expect(init_data[:segments].keys).to include('my-segment') - expect(init_data[:segments]['my-segment'][:key]).to eq('my-segment') - expect(init_data[:segments]['my-segment'][:version]).to eq(1) - expect(init_data[:segments]['my-segment'][:included]).to eq(['user1']) - expect(init_data[:segments]['my-segment'][:excluded]).to eq(['user2']) + expect(init_data[:segments].keys).to include(:mysegment) + expect(init_data[:segments][:mysegment][:key]).to eq('mysegment') + expect(init_data[:segments][:mysegment][:version]).to eq(1) + expect(init_data[:segments][:mysegment][:included]).to eq(['user1']) + expect(init_data[:segments][:mysegment][:excluded]).to eq(['user2']) end it 'increments segment version on update' do td = TestDataV2.data_source - td.use_preconfigured_segment({ key: 'my-segment', version: 100 }) - td.use_preconfigured_segment({ key: 'my-segment', included: ['user2'] }) + td.use_preconfigured_segment({ key: 'mysegment', version: 100 }) + td.use_preconfigured_segment({ key: 'mysegment', included: ['user2'] }) init_data = td.make_init_data - expect(init_data[:segments]['my-segment'][:version]).to eq(2) - expect(init_data[:segments]['my-segment'][:included]).to eq(['user2']) + expect(init_data[:segments][:mysegment][:version]).to eq(2) + expect(init_data[:segments][:mysegment][:included]).to eq(['user2']) end describe 'TestDataSourceV2' do it 'includes both flags and segments in fetch' do td = TestDataV2.data_source - td.update(td.flag('my-flag').variation_for_all(true)) - td.use_preconfigured_segment({ key: 'my-segment', included: ['user1'] }) + td.update(td.flag('myflag').variation_for_all(true)) + td.use_preconfigured_segment({ key: 'mysegment', included: ['user1'] }) source = LaunchDarkly::Impl::Integrations::TestData::TestDataSourceV2.new(td) result = source.fetch(nil) @@ -71,10 +71,10 @@ module Integrations segment_change = change_set.changes.detect { |c| c.kind == LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT } expect(flag_change).not_to be_nil - expect(flag_change.key).to eq('my-flag') + expect(flag_change.key).to eq(:myflag) expect(segment_change).not_to be_nil - expect(segment_change.key).to eq('my-segment') + expect(segment_change.key).to eq(:mysegment) end it 'propagates segment updates' do @@ -94,7 +94,7 @@ module Integrations sleep 0.1 # Add a segment - td.use_preconfigured_segment({ key: 'test-segment', included: ['user1'] }) + td.use_preconfigured_segment({ key: 'testsegment', included: ['user1'] }) # Wait for the update to propagate sync_thread.join(1) @@ -107,7 +107,7 @@ module Integrations # Check that the second update contains the segment segment_change = updates[1].change_set.changes.detect { |c| c.kind == LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT } expect(segment_change).not_to be_nil - expect(segment_change.key).to eq('test-segment') + expect(segment_change.key).to eq(:testsegment) end end end