diff --git a/lib/splitclient-rb/cache/fetchers/split_fetcher.rb b/lib/splitclient-rb/cache/fetchers/split_fetcher.rb index c2772d62..c1cba9dc 100644 --- a/lib/splitclient-rb/cache/fetchers/split_fetcher.rb +++ b/lib/splitclient-rb/cache/fetchers/split_fetcher.rb @@ -25,7 +25,7 @@ def call def fetch_splits(fetch_options = { cache_control_headers: false, till: nil }) @semaphore.synchronize do data = splits_since(@splits_repository.get_change_number, @rule_based_segments_repository.get_change_number, fetch_options) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(@splits_repository, data[:ff][:d], data[:ff][:t], @config) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(@splits_repository, data[:ff][:d], data[:ff][:t], @config, @splits_api.clear_storage) SplitIoClient::Helpers::RepositoryHelper.update_rule_based_segment_repository(@rule_based_segments_repository, data[:rbs][:d], data[:rbs][:t], @config) @splits_repository.set_segment_names(data[:segment_names]) @rule_based_segments_repository.set_segment_names(data[:segment_names]) diff --git a/lib/splitclient-rb/cache/repositories/splits_repository.rb b/lib/splitclient-rb/cache/repositories/splits_repository.rb index e98e0d84..2fa7c396 100644 --- a/lib/splitclient-rb/cache/repositories/splits_repository.rb +++ b/lib/splitclient-rb/cache/repositories/splits_repository.rb @@ -43,10 +43,7 @@ def initialize(config, flag_sets_repository, flag_set_filter) end @flag_sets = flag_sets_repository @flag_set_filter = flag_set_filter - unless @config.mode.equal?(:consumer) - @adapter.set_string(namespace_key('.splits.till'), '-1') - @adapter.initialize_map(namespace_key('.segments.registered')) - end + initialize_keys end def update(to_add, to_delete, new_change_number) @@ -127,6 +124,7 @@ def clear @tt_cache.clear @adapter.clear(namespace_key) + initialize_keys end def kill(change_number, split_name, default_treatment) @@ -167,6 +165,13 @@ def flag_set_filter private + def initialize_keys + unless @config.mode.equal?(:consumer) + @adapter.set_string(namespace_key('.splits.till'), '-1') + @adapter.initialize_map(namespace_key('.segments.registered')) + end + end + def add_feature_flag(split) return unless split[:name] existing_split = get_split(split[:name]) diff --git a/lib/splitclient-rb/engine/api/client.rb b/lib/splitclient-rb/engine/api/client.rb index 65dcd027..d9d6e0df 100644 --- a/lib/splitclient-rb/engine/api/client.rb +++ b/lib/splitclient-rb/engine/api/client.rb @@ -50,6 +50,9 @@ def post_api(url, api_key, data, headers = {}, params = {}) raise e, 'Split SDK failed to connect to backend to post information', e.backtrace end + def sdk_url_overriden? + @config.sdk_url_overriden? + end private def api_client diff --git a/lib/splitclient-rb/engine/api/splits.rb b/lib/splitclient-rb/engine/api/splits.rb index 0596c9de..e00a76a0 100644 --- a/lib/splitclient-rb/engine/api/splits.rb +++ b/lib/splitclient-rb/engine/api/splits.rb @@ -5,28 +5,72 @@ module Api # Retrieves split definitions from the Split Backend class Splits < Client + PROXY_CHECK_INTERVAL_SECONDS = 24 * 60 * 60 + SPEC_1_1 = "1.1" + def initialize(api_key, config, telemetry_runtime_producer) super(config) @api_key = api_key @telemetry_runtime_producer = telemetry_runtime_producer @flag_sets_filter = @config.flag_sets_filter + @spec_version = SplitIoClient::Spec::FeatureFlags::SPEC_VERSION + @last_proxy_check_timestamp = 0 + @clear_storage = false + @old_spec_since = nil end def since(since, since_rbs, fetch_options = { cache_control_headers: false, till: nil, sets: nil}) start = Time.now + + if check_last_proxy_check_timestamp + @spec_version = SplitIoClient::Spec::FeatureFlags::SPEC_VERSION + @config.logger.debug("Switching to new Feature flag spec #{@spec_version} and fetching.") + @old_spec_since = since + since = -1 + since_rbs = -1 + fetch_options = { cache_control_headers: false, till: nil, sets: nil} + end + + if @spec_version == Splits::SPEC_1_1 + since = @old_spec_since unless @old_spec_since.nil? + params = { s: @spec_version, since: since } + @old_spec_since = nil + else + params = { s: @spec_version, since: since, rbSince: since_rbs } + end - params = { s: SplitIoClient::Spec::FeatureFlags::SPEC_VERSION, since: since, rbSince: since_rbs } params[:sets] = @flag_sets_filter.join(",") unless @flag_sets_filter.empty? params[:till] = fetch_options[:till] unless fetch_options[:till].nil? @config.logger.debug("Fetching from splitChanges with #{params}: ") response = get_api("#{@config.base_uri}/splitChanges", @api_key, params, fetch_options[:cache_control_headers]) + if response.status == 414 @config.logger.error("Error fetching feature flags; the amount of flag sets provided are too big, causing uri length error.") raise ApiException.new response.body, 414 end + + if response.status == 400 and sdk_url_overriden? and @spec_version == SplitIoClient::Spec::FeatureFlags::SPEC_VERSION + @config.logger.warn("Detected proxy response error, changing spec version from #{@spec_version} to #{Splits::SPEC_1_1} and re-fetching.") + @spec_version = Splits::SPEC_1_1 + @last_proxy_check_timestamp = Time.now + return since(since, 0, fetch_options = {cache_control_headers: fetch_options[:cache_control_headers], till: fetch_options[:till], + sets: fetch_options[:sets]}) + end + if response.success? - result = objects_with_segment_names(response.body) + result = JSON.parse(response.body, symbolize_names: true) + if @spec_version == Splits::SPEC_1_1 + result = convert_to_newSPEC(result) + end + + result[:rbs][:d] = check_rbs_data(result[:rbs][:d]) + result = objects_with_segment_names(result) + if @spec_version == SplitIoClient::Spec::FeatureFlags::SPEC_VERSION + @clear_storage = @last_proxy_check_timestamp != 0 + @last_proxy_check_timestamp = 0 + end + unless result[:ff][:d].empty? @config.split_logger.log_if_debug("#{result[:ff][:d].length} feature flags retrieved. since=#{since}") end @@ -52,10 +96,22 @@ def since(since, since_rbs, fetch_options = { cache_control_headers: false, till end end + def clear_storage + @clear_storage + end + private - def objects_with_segment_names(objects_json) - parsed_objects = JSON.parse(objects_json, symbolize_names: true) + def check_rbs_data(rbs_data) + rbs_data.each do |rb_segment| + rb_segment[:excluded] = {:keys => [], :segments => []} if rb_segment[:excluded].nil? + rb_segment[:excluded][:keys] = [] if rb_segment[:excluded][:keys].nil? + rb_segment[:excluded][:segments] = [] if rb_segment[:excluded][:segments].nil? + end + rbs_data + end + + def objects_with_segment_names(parsed_objects) parsed_objects[:segment_names] = Set.new parsed_objects[:segment_names] = parsed_objects[:ff][:d].each_with_object(Set.new) do |split, splits| @@ -63,19 +119,19 @@ def objects_with_segment_names(objects_json) end.flatten parsed_objects[:rbs][:d].each do |rule_based_segment| - parsed_objects[:segment_names].merge Helpers::Util.segment_names_by_object(rule_based_segment, "IN_SEGMENT") - end - - parsed_objects[:rbs][:d].each do |rule_based_segment| - rule_based_segment[:excluded][:segments].each do |segment| - if segment[:type] == "standard" - parsed_objects[:segment_names].add(segment[:name]) - end - end + parsed_objects[:segment_names].merge Helpers::Util.segment_names_in_rb_segment(rule_based_segment, "IN_SEGMENT") end parsed_objects end + + def check_last_proxy_check_timestamp + @spec_version == Splits::SPEC_1_1 and ((Time.now - @last_proxy_check_timestamp) >= Splits::PROXY_CHECK_INTERVAL_SECONDS) + end + + def convert_to_newSPEC(body) + {:ff => {:d => body[:splits], :s => body[:since], :t => body[:till]}, :rbs => {:d => [], :s => -1, :t => -1}} + end end end end diff --git a/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb b/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb index aa2cefb9..5143f606 100644 --- a/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb +++ b/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb @@ -25,20 +25,10 @@ def match?(args) rule_based_segment = @rule_based_segments_repository.get_rule_based_segment(@segment_name) return false if rule_based_segment.nil? - if args[:value].nil? or args[:value].empty? - key = args[:matching_key] - else - key = args[:value] - end + key = update_key(args) return false if rule_based_segment[:excluded][:keys].include?(key) - rule_based_segment[:excluded][:segments].each do |segment| - return false if segment[:type] == 'standard' and @segments_repository.in_segment?(segment[:name], key) - - if segment[:type] == 'rule-based' - return false if match_rbs(@rule_based_segments_repository.get_rule_based_segment(segment[:name]), args) - end - end + return false unless check_excluded_segments(rule_based_segment, key, args) matches = false rule_based_segment[:conditions].each do |c| @@ -52,13 +42,31 @@ def match?(args) private - def match_rbs(rule_based_segment, args) - rbs_matcher = RuleBasedSegmentMatcher.new(@segments_repository, @rule_based_segments_repository, rule_based_segment[:name], @config) - return rbs_matcher.match?( - matching_key: args[:matching_key], - bucketing_key: args[:value], - attributes: args[:attributes] - ) + def check_excluded_segments(rule_based_segment, key, args) + rule_based_segment[:excluded][:segments].each do |segment| + return false if segment[:type] == 'standard' && @segments_repository.in_segment?(segment[:name], key) + + return false if segment[:type] == 'rule-based' && match_rbs( + @rule_based_segments_repository.get_rule_based_segment(segment[:name]), args + ) + end + true + end + + def update_key(args) + if args[:value].nil? || args[:value].empty? + args[:matching_key] + else + args[:value] + end + end + + def match_rbs(rule_based_segment, args) + rbs_matcher = RuleBasedSegmentMatcher.new(@segments_repository, @rule_based_segments_repository, + rule_based_segment[:name], @config) + rbs_matcher.match?(matching_key: args[:matching_key], + bucketing_key: args[:value], + attributes: args[:attributes]) end end end diff --git a/lib/splitclient-rb/helpers/evaluator_helper.rb b/lib/splitclient-rb/helpers/evaluator_helper.rb index 331c7987..2b16e094 100644 --- a/lib/splitclient-rb/helpers/evaluator_helper.rb +++ b/lib/splitclient-rb/helpers/evaluator_helper.rb @@ -8,7 +8,9 @@ def self.matcher_type(condition, segments_repository, rb_segment_repository) segments_repository.adapter.pipelined do condition.matchers.each do |matcher| matchers << if matcher[:negate] - condition.negation_matcher(matcher_instance(matcher[:matcherType], condition, matcher, segments_repository, rb_segment_repository)) + condition.negation_matcher(matcher_instance(matcher[:matcherType], condition, + matcher, segments_repository, + rb_segment_repository)) else matcher_instance(matcher[:matcherType], condition, matcher, segments_repository, rb_segment_repository) end diff --git a/lib/splitclient-rb/helpers/repository_helper.rb b/lib/splitclient-rb/helpers/repository_helper.rb index 24fdd76b..da72ad22 100644 --- a/lib/splitclient-rb/helpers/repository_helper.rb +++ b/lib/splitclient-rb/helpers/repository_helper.rb @@ -3,7 +3,7 @@ module SplitIoClient module Helpers class RepositoryHelper - def self.update_feature_flag_repository(feature_flag_repository, feature_flags, change_number, config) + def self.update_feature_flag_repository(feature_flag_repository, feature_flags, change_number, config, clear_storage) to_add = [] to_delete = [] feature_flags.each do |feature_flag| @@ -13,19 +13,25 @@ def self.update_feature_flag_repository(feature_flag_repository, feature_flags, next end - unless feature_flag.key?(:impressionsDisabled) - feature_flag[:impressionsDisabled] = false - if config.debug_enabled - config.logger.debug("feature flag (#{feature_flag[:name]}) does not have impressionsDisabled field, setting it to false") - end - end + feature_flag = check_impressions_disabled(feature_flag, config) config.logger.debug("storing feature flag (#{feature_flag[:name]})") if config.debug_enabled to_add.push(feature_flag) end + feature_flag_repository.clear if clear_storage feature_flag_repository.update(to_add, to_delete, change_number) end + def self.check_impressions_disabled(feature_flag, config) + unless feature_flag.key?(:impressionsDisabled) + feature_flag[:impressionsDisabled] = false + if config.debug_enabled + config.logger.debug("feature flag (#{feature_flag[:name]}) does not have impressionsDisabled field, setting it to false") + end + end + feature_flag + end + def self.update_rule_based_segment_repository(rule_based_segment_repository, rule_based_segments, change_number, config) to_add = [] to_delete = [] @@ -39,6 +45,7 @@ def self.update_rule_based_segment_repository(rule_based_segment_repository, rul config.logger.debug("storing rule based segment (#{rule_based_segment[:name]})") if config.debug_enabled to_add.push(rule_based_segment) end + rule_based_segment_repository.update(to_add, to_delete, change_number) end end diff --git a/lib/splitclient-rb/helpers/util.rb b/lib/splitclient-rb/helpers/util.rb index c9b356c4..3c6eff85 100644 --- a/lib/splitclient-rb/helpers/util.rb +++ b/lib/splitclient-rb/helpers/util.rb @@ -6,11 +6,23 @@ class Util def self.segment_names_by_object(object, matcher_type) object[:conditions].each_with_object(Set.new) do |condition, names| condition[:matcherGroup][:matchers].each do |matcher| - next if matcher[:userDefinedSegmentMatcherData].nil? or matcher[:matcherType] != matcher_type + next if matcher[:userDefinedSegmentMatcherData].nil? || matcher[:matcherType] != matcher_type + names << matcher[:userDefinedSegmentMatcherData][:segmentName] end end end + + def self.segment_names_in_rb_segment(object, matcher_type) + names = Set.new + names.merge segment_names_by_object(object, matcher_type) + object[:excluded][:segments].each do |segment| + if segment[:type] == 'standard' + names.add(segment[:name]) + end + end + names + end end end end diff --git a/lib/splitclient-rb/split_config.rb b/lib/splitclient-rb/split_config.rb index da434c91..de7f3e4d 100644 --- a/lib/splitclient-rb/split_config.rb +++ b/lib/splitclient-rb/split_config.rb @@ -645,6 +645,10 @@ def consumer? @mode.equal?(:consumer) end + def sdk_url_overriden? + return @base_uri != SplitConfig.default_base_uri + end + # # gets the hostname where the sdk gem is running # diff --git a/lib/splitclient-rb/sse/notification_processor.rb b/lib/splitclient-rb/sse/notification_processor.rb index d723cbdc..af5563c3 100644 --- a/lib/splitclient-rb/sse/notification_processor.rb +++ b/lib/splitclient-rb/sse/notification_processor.rb @@ -27,7 +27,7 @@ def process(incoming_notification) private def process_split_update(notification) - @config.logger.debug("#{notification.type} notification received: #{notification}") if @config.debug_enabled + @config.logger.debug("#{notification.event_type} notification received: #{notification}") if @config.debug_enabled @splits_worker.add_to_queue(notification) end diff --git a/lib/splitclient-rb/sse/workers/splits_worker.rb b/lib/splitclient-rb/sse/workers/splits_worker.rb index 419a3814..4fe7563e 100644 --- a/lib/splitclient-rb/sse/workers/splits_worker.rb +++ b/lib/splitclient-rb/sse/workers/splits_worker.rb @@ -4,7 +4,8 @@ module SplitIoClient module SSE module Workers class SplitsWorker - def initialize(synchronizer, config, feature_flags_repository, telemetry_runtime_producer, segment_fetcher, rule_based_segment_repository) + def initialize(synchronizer, config, feature_flags_repository, telemetry_runtime_producer, + segment_fetcher, rule_based_segment_repository) @synchronizer = synchronizer @config = config @feature_flags_repository = feature_flags_repository @@ -68,12 +69,11 @@ def perform def update_feature_flag(notification) return true if @feature_flags_repository.get_change_number.to_i >= notification.data['changeNumber'] return false unless !notification.data['d'].nil? && @feature_flags_repository.get_change_number == notification.data['pcn'] - new_split = return_object_from_json(notification) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(@feature_flags_repository, - [new_split], - notification.data['changeNumber'], @config) - fetch_segments_if_not_exists(Helpers::Util.segment_names_by_object(new_split, "IN_SEGMENT"), @feature_flags_repository) - if fetch_rule_based_segments_if_not_exists(Helpers::Util.segment_names_by_object(new_split, "IN_RULE_BASED_SEGMENT"), notification.data['changeNumber']) + + new_split = update_feature_flag_repository(notification) + fetch_segments_if_not_exists(Helpers::Util.segment_names_by_object(new_split, 'IN_SEGMENT'), @feature_flags_repository) + if fetch_rule_based_segments_if_not_exists(Helpers::Util.segment_names_by_object(new_split, 'IN_RULE_BASED_SEGMENT'), + notification.data['changeNumber']) return true end @@ -86,18 +86,27 @@ def update_feature_flag(notification) false end + def update_feature_flag_repository(notification) + new_split = return_object_from_json(notification) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(@feature_flags_repository, [new_split], + notification.data['changeNumber'], @config, false) + new_split + end + def update_rule_based_segment(notification) return true if @rule_based_segment_repository.get_change_number.to_i >= notification.data['changeNumber'] - return false unless !notification.data['d'].nil? && @rule_based_segment_repository.get_change_number == notification.data['pcn'] + return false unless !notification.data['d'].nil? && + @rule_based_segment_repository.get_change_number == notification.data['pcn'] new_rb_segment = return_object_from_json(notification) SplitIoClient::Helpers::RepositoryHelper.update_rule_based_segment_repository(@rule_based_segment_repository, - [new_rb_segment], - notification.data['changeNumber'], @config) - fetch_segments_if_not_exists(Helpers::Util.segment_names_by_object(new_rb_segment, "IN_SEGMENT"), @rule_based_segment_repository) + [new_rb_segment], + notification.data['changeNumber'], @config) + fetch_segments_if_not_exists(Helpers::Util.segment_names_in_rb_segment(new_rb_segment, 'IN_SEGMENT'), + @rule_based_segment_repository) -# TODO: enable when telemetry spec is added -# @telemetry_runtime_producer.record_updates_from_sse(Telemetry::Domain::Constants::SPLITS) + # TODO: enable when telemetry spec is added + # @telemetry_runtime_producer.record_updates_from_sse(Telemetry::Domain::Constants::SPLITS) true rescue StandardError => e @@ -110,11 +119,9 @@ def kill_feature_flag(notification) return if @feature_flags_repository.get_change_number.to_i > notification.data['changeNumber'] @config.logger.debug("feature_flags_worker kill #{notification.data['splitName']}, #{notification.data['changeNumber']}") - @feature_flags_repository.kill( - notification.data['changeNumber'], - notification.data['splitName'], - notification.data['defaultTreatment'] - ) + @feature_flags_repository.kill(notification.data['changeNumber'], + notification.data['splitName'], + notification.data['defaultTreatment']) @synchronizer.fetch_splits(notification.data['changeNumber'], 0) end @@ -124,7 +131,6 @@ def return_object_from_json(notification) end def fetch_segments_if_not_exists(segment_names, object_repository) - return if segment_names.nil? object_repository.set_segment_names(segment_names) @@ -132,9 +138,8 @@ def fetch_segments_if_not_exists(segment_names, object_repository) end def fetch_rule_based_segments_if_not_exists(segment_names, change_number) - if segment_names.nil? or segment_names.empty? or @rule_based_segment_repository.contains?(segment_names.to_a) - return false - end + return false if segment_names.nil? || segment_names.empty? || @rule_based_segment_repository.contains?(segment_names.to_a) + @synchronizer.fetch_splits(0, change_number) true diff --git a/spec/engine/api/splits_spec.rb b/spec/engine/api/splits_spec.rb index af20bd9a..e5c4a47f 100644 --- a/spec/engine/api/splits_spec.rb +++ b/spec/engine/api/splits_spec.rb @@ -21,7 +21,7 @@ stub_request(:get, 'https://sdk.split.io/api/splitChanges?s=1.3&since=-1&rbSince=-1') .to_return(status: 200, body: splits) - parsed_splits = splits_api.send(:objects_with_segment_names, splits) + parsed_splits = splits_api.send(:objects_with_segment_names, JSON.parse(splits, symbolize_names: true)) expect(parsed_splits[:segment_names]).to eq(Set.new(%w[demo employees])) end @@ -183,4 +183,80 @@ ) end end + + context 'old spec tests' do + let(:old_spec_splits) { File.read(File.expand_path(File.join(File.dirname(__FILE__), '../../test_data/rule_based_segments/split_old_spec.json'))) } + let(:config) do + SplitIoClient::SplitConfig.new( + logger: Logger.new(log), + debug_enabled: true, + transport_debug_enabled: true, + base_uri: "https://proxy-server/api" + ) + end + let(:log) { StringIO.new } + let(:telemetry_runtime_producer) { SplitIoClient::Telemetry::RuntimeProducer.new(config) } + let(:splits_api) { described_class.new('', config, telemetry_runtime_producer) } + + it 'switch to old spec url whith proper conditions' do + stub_request(:get, 'https://proxy-server/api/splitChanges?s=1.3&since=-1&rbSince=-1') + .to_return(status: 400, body: '') + stub_request(:get, 'https://proxy-server/api/splitChanges?s=1.1&since=-1') + .to_return(status: 200, body: old_spec_splits) + + parsed_splits = splits_api.since(-1, -1) + + expect(parsed_splits[:ff][:d].length()).to eq(6) + expect(parsed_splits[:ff][:t]).to eq(1457726098069) + expect(parsed_splits[:ff][:s]).to eq(-1) + expect(parsed_splits[:rbs]).to eq({:d => [], :s => -1, :t => -1}) + expect(splits_api.clear_storage).to eq(false) + end + + it 'check new spec after last proxy timestamp expires' do + stub_request(:get, 'https://proxy-server/api/splitChanges?s=1.3&since=-1&rbSince=-1') + .to_return({status: 400, body: ''}, {status: 200, body: splits}) + stub_request(:get, 'https://proxy-server/api/splitChanges?s=1.1&since=-1') + .to_return(status: 200, body: old_spec_splits) + + parsed_splits = splits_api.since(-1, -1) + expect(parsed_splits[:ff][:d].length()).to eq(6) + expect(splits_api.instance_variable_get(:@spec_version)).to eq(SplitIoClient::Api::Splits::SPEC_1_1) + + SplitIoClient::Api::Splits::PROXY_CHECK_INTERVAL_SECONDS = 1 + sleep 1 + parsed_splits = splits_api.since(1457726098069, -1) + expect(splits_api.clear_storage).to eq(true) + expect(parsed_splits[:ff][:d].length()).to eq(2) + expect(parsed_splits[:rbs][:d].length()).to eq(1) + expect(splits_api.instance_variable_get(:@spec_version)).to eq(SplitIoClient::Spec::FeatureFlags::SPEC_VERSION) + expect(splits_api.instance_variable_get(:@old_spec_since)).to eq(1457726098069) + end + + it 'check using old_spec_since variable' do + old_spec_splits2 = File.read(File.expand_path(File.join(File.dirname(__FILE__), '../../test_data/rule_based_segments/split_old_spec2.json'))) + + stub_request(:get, 'https://proxy-server/api/splitChanges?s=1.3&since=-1&rbSince=-1') + .to_return({status: 400, body: ''}, {status: 400, body: ''}) + stub_request(:get, 'https://proxy-server/api/splitChanges?s=1.1&since=-1') + .to_return(status: 200, body: old_spec_splits) + stub_request(:get, 'https://proxy-server/api/splitChanges?s=1.1&since=1457726098069') + .to_return(status: 200, body: old_spec_splits2) + + parsed_splits = splits_api.since(-1, -1) + expect(parsed_splits[:ff][:d].length()).to eq(6) + expect(splits_api.instance_variable_get(:@spec_version)).to eq(SplitIoClient::Api::Splits::SPEC_1_1) + + SplitIoClient::Api::Splits::PROXY_CHECK_INTERVAL_SECONDS = 1 + sleep 1 + parsed_splits = splits_api.since(1457726098069, -1) + SplitIoClient::Api::Splits::PROXY_CHECK_INTERVAL_SECONDS = 100000 + + sleep 1 + expect(splits_api.instance_variable_get(:@spec_version)).to eq(SplitIoClient::Api::Splits::SPEC_1_1) + expect(splits_api.instance_variable_get(:@old_spec_since)).to eq(nil) + expect(parsed_splits[:ff][:d].length()).to eq(1) + expect(log.string).to include 'Switching to new Feature flag spec 1.3 and fetching.' + end + end end diff --git a/spec/integrations/in_memory_client_spec.rb b/spec/integrations/in_memory_client_spec.rb index ab0538ae..2f2024b2 100644 --- a/spec/integrations/in_memory_client_spec.rb +++ b/spec/integrations/in_memory_client_spec.rb @@ -1381,6 +1381,44 @@ expect(client_rbs.get_treatment('mauro@split.io', 'rbs_feature_flag', {:email => 'mauro@split.io'})).to eq('off') end end + + context 'old spec tests' do + let(:old_spec_splits) { File.read(File.expand_path(File.join(File.dirname(__FILE__), '../test_data/rule_based_segments/split_old_spec.json'))) } + + it 'check new spec after last proxy timestamp expires' do + splits_rbs = File.read(File.join(SplitIoClient.root, 'spec/test_data/rule_based_segments/rule_base_segments.json')) + + stub_request(:get, 'https://proxy-server/api/splitChanges?s=1.3&since=-1&rbSince=-1') + .to_return({status: 400, body: ''}, {status: 200, body: splits_rbs}) + stub_request(:get, "https://sdk.split.io/api/splitChanges?rbSince=1506703262916&s=1.3&since=1506703262916") + .to_return(status: 200, body: '') + stub_request(:get, 'https://proxy-server/api/splitChanges?s=1.1&since=-1') + .to_return(status: 200, body: old_spec_splits) + stub_request(:get, "https://proxy-server/api/splitChanges?s=1.1&since=1457726098069") + .to_return(status: 200, body: '') + stub_request(:post, "https://telemetry.split.io/api/v1/metrics/config") + .to_return(status: 200, body: '') + + factory_old_spec = + SplitIoClient::SplitFactory.new('test_api_key', + {impressions_mode: :none, + features_refresh_rate: 2, + base_uri: "https://proxy-server/api", + streaming_enabled: false}) + + SplitIoClient::Api::Splits::PROXY_CHECK_INTERVAL_SECONDS = 1 + client_old_spec = factory_old_spec.client + client_old_spec.block_until_ready + expect(client_old_spec.get_treatment('whitelisted_user', 'whitelist_feature')).to eq('on') + + sleep 1 + split_fetcher = factory_old_spec.instance_variable_get(:@split_fetcher) + split_fetcher.fetch_splits + sleep 1 + expect(client_old_spec.get_treatment('bilal@split.io', 'rbs_feature_flag', {:email => 'bilal@split.io'})).to eq('on') + expect(client_old_spec.get_treatment('whitelisted_user', 'whitelist_feature')).to eq('control') + end + end end private diff --git a/spec/repository_helper.rb b/spec/repository_helper.rb index 0f091f78..d38dff96 100644 --- a/spec/repository_helper.rb +++ b/spec/repository_helper.rb @@ -13,16 +13,16 @@ flag_sets_repository, flag_set_filter) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ACTIVE', conditions: [], :sets => []}], -1, config) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ACTIVE', conditions: [], :sets => []}], -1, config, false) expect(feature_flag_repository.get_split('split1').nil?).to eq(true) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ACTIVE', conditions: [], :sets => ['set_3']}], -1, config) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ACTIVE', conditions: [], :sets => ['set_3']}], -1, config, false) expect(feature_flag_repository.get_split('split1').nil?).to eq(true) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ACTIVE', conditions: [], :sets => ['set_1']}], -1, config) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ACTIVE', conditions: [], :sets => ['set_1']}], -1, config, false) expect(feature_flag_repository.get_split('split1').nil?).to eq(false) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ARCHIVED', conditions: [], :sets => ['set_1']}], -1, config) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ARCHIVED', conditions: [], :sets => ['set_1']}], -1, config, false) expect(feature_flag_repository.get_split('split1').nil?).to eq(true) end @@ -35,16 +35,16 @@ flag_sets_repository, flag_set_filter) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ACTIVE', conditions: [], :sets => []}], -1, config) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ACTIVE', conditions: [], :sets => []}], -1, config, false) expect(feature_flag_repository.get_split('split1').nil?).to eq(false) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split2', :status => 'ACTIVE', conditions: [], :sets => ['set_3']}], -1, config) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split2', :status => 'ACTIVE', conditions: [], :sets => ['set_3']}], -1, config, false) expect(feature_flag_repository.get_split('split2').nil?).to eq(false) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split3', :status => 'ACTIVE', conditions: [], :sets => ['set_1']}], -1, config) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split3', :status => 'ACTIVE', conditions: [], :sets => ['set_1']}], -1, config, false) expect(feature_flag_repository.get_split('split1').nil?).to eq(false) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ARCHIVED', conditions: [], :sets => ['set_1']}], -1, config) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ARCHIVED', conditions: [], :sets => ['set_1']}], -1, config, false) expect(feature_flag_repository.get_split('split1').nil?).to eq(true) end @@ -57,16 +57,31 @@ flag_sets_repository, flag_set_filter) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ACTIVE', conditions: [], :sets => []}], -1, config) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ACTIVE', conditions: [], :sets => []}], -1, config, false) expect(feature_flag_repository.get_split('split1')[:impressionsDisabled]).to eq(false) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split2', :status => 'ACTIVE', conditions: [], :impressionsDisabled => false, :sets => []}], -1, config) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split2', :status => 'ACTIVE', conditions: [], :impressionsDisabled => false, :sets => []}], -1, config, false) expect(feature_flag_repository.get_split('split2')[:impressionsDisabled]).to eq(false) - SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split2', :status => 'ACTIVE', conditions: [], :impressionsDisabled => true, :sets => []}], -1, config) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split2', :status => 'ACTIVE', conditions: [], :impressionsDisabled => true, :sets => []}], -1, config, false) expect(feature_flag_repository.get_split('split2')[:impressionsDisabled]).to eq(true) + end + + it 'test clear cache flag' do + config = SplitIoClient::SplitConfig.new(cache_adapter: :memory) + flag_set_filter = SplitIoClient::Cache::Filter::FlagSetsFilter.new([]) + flag_sets_repository = SplitIoClient::Cache::Repositories::MemoryFlagSetsRepository.new([]) + feature_flag_repository = SplitIoClient::Cache::Repositories::SplitsRepository.new( + config, + flag_sets_repository, + flag_set_filter) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split1', :status => 'ACTIVE', conditions: [], :sets => []}], -1, config, false) + expect(feature_flag_repository.get_split('split1').nil?).to eq(false) + SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(feature_flag_repository, [{:name => 'split2', :status => 'ACTIVE', conditions: [], :sets => ['set_3']}], -1, config, true) + expect(feature_flag_repository.get_split('split2').nil?).to eq(false) + expect(feature_flag_repository.get_split('split1').nil?).to eq(true) end end end diff --git a/spec/sse/workers/splits_worker_spec.rb b/spec/sse/workers/splits_worker_spec.rb index 0ef724f9..74e00c51 100644 --- a/spec/sse/workers/splits_worker_spec.rb +++ b/spec/sse/workers/splits_worker_spec.rb @@ -22,7 +22,7 @@ let(:event_split_update_no_definition) { SplitIoClient::SSE::EventSource::StreamData.new("data", 123, JSON.parse('{"type":"SPLIT_UPDATE","changeNumber":5564531221,"pcn":1234,"c": 0, "d":null}'), 'test') } let(:event_split_update_segments) { SplitIoClient::SSE::EventSource::StreamData.new("data", 123, JSON.parse('{"type":"SPLIT_UPDATE","changeNumber":5564531221,"pcn":1234,"c":2,"d":"eJzcVFtr20wQ/SvhPK9AsnzTvpnPJp+po0DlppRgzFga2dusJLNapaRG/73Id7sOoU+FvmluZ3TOGXYDayhNVTx9W3NIGUOiKtlAQCWQSNq+FyeJ6yzcBTuex+T0qe86XrfrUkJBzH4AgXw3mVFlivl3eiWIA/BA6yImq4oc0nPdG/mIOYF0gpYfeO3AEyh3Ca/XDfxer+u2BUpLtiohMfhvOn4aQeBFad20paRLFkg4pUrbqWGyGecWEvbwPQ9cCMQrypccVtmCDaTX7feCnu+7nY7nCZBeFpAtgbjIU7WszPbPSshNvc0lah8/b05hoxkkvv4/no4m42gKgYxsvGJzb4pqDdn0ZguVNwsxCIenhh3SPriBk/OSLB/Z/Vgpy1qV9mE3MSRLDfwxD/kMSjKVb1dUpmgwVFxgVtezWmBNxp5RsDdlavkdCJTqJ2+tqmcCmhasIU+LOEEtftfg8+Nk8vjlzxV44beINce2ME3z2TEeDrEWVzKNw3k0un8YhTd0aiaGnKqck4iXDakrwcpdNjzdq9PChxIV+VEXt2F/UUvTC9Guyk/t90dfO+/Xro73w65z7y6cU/ndnvTdge7f9W8wmcw/jb5F1+79yybsX6c7U2lGPat/BQAA//9ygdKB"}'), 'test') } let(:event_split_update_rb_segments) { SplitIoClient::SSE::EventSource::StreamData.new("data", 123, JSON.parse('{"type":"SPLIT_UPDATE","changeNumber":5564531221,"pcn":1234,"c":0,"d":"eyJjaGFuZ2VOdW1iZXIiOiAxMCwgInRyYWZmaWNUeXBlTmFtZSI6ICJ1c2VyIiwgIm5hbWUiOiAicmJzX2ZsYWciLCAidHJhZmZpY0FsbG9jYXRpb24iOiAxMDAsICJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOiAxODI4Mzc3MzgwLCAic2VlZCI6IC0yODY2MTc5MjEsICJzdGF0dXMiOiAiQUNUSVZFIiwgImtpbGxlZCI6IGZhbHNlLCAiZGVmYXVsdFRyZWF0bWVudCI6ICJvZmYiLCAiYWxnbyI6IDIsICJjb25kaXRpb25zIjogW3siY29uZGl0aW9uVHlwZSI6ICJST0xMT1VUIiwgIm1hdGNoZXJHcm91cCI6IHsiY29tYmluZXIiOiAiQU5EIiwgIm1hdGNoZXJzIjogW3sia2V5U2VsZWN0b3IiOiB7InRyYWZmaWNUeXBlIjogInVzZXIifSwgIm1hdGNoZXJUeXBlIjogIklOX1JVTEVfQkFTRURfU0VHTUVOVCIsICJuZWdhdGUiOiBmYWxzZSwgInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjogeyJzZWdtZW50TmFtZSI6ICJzYW1wbGVfcnVsZV9iYXNlZF9zZWdtZW50In19XX0sICJwYXJ0aXRpb25zIjogW3sidHJlYXRtZW50IjogIm9uIiwgInNpemUiOiAxMDB9LCB7InRyZWF0bWVudCI6ICJvZmYiLCAic2l6ZSI6IDB9XSwgImxhYmVsIjogImluIHJ1bGUgYmFzZWQgc2VnbWVudCBzYW1wbGVfcnVsZV9iYXNlZF9zZWdtZW50In0sIHsiY29uZGl0aW9uVHlwZSI6ICJST0xMT1VUIiwgIm1hdGNoZXJHcm91cCI6IHsiY29tYmluZXIiOiAiQU5EIiwgIm1hdGNoZXJzIjogW3sia2V5U2VsZWN0b3IiOiB7InRyYWZmaWNUeXBlIjogInVzZXIifSwgIm1hdGNoZXJUeXBlIjogIkFMTF9LRVlTIiwgIm5lZ2F0ZSI6IGZhbHNlfV19LCAicGFydGl0aW9ucyI6IFt7InRyZWF0bWVudCI6ICJvbiIsICJzaXplIjogMH0sIHsidHJlYXRtZW50IjogIm9mZiIsICJzaXplIjogMTAwfV0sICJsYWJlbCI6ICJkZWZhdWx0IHJ1bGUifV0sICJjb25maWd1cmF0aW9ucyI6IHt9LCAic2V0cyI6IFtdLCAiaW1wcmVzc2lvbnNEaXNhYmxlZCI6IGZhbHNlfQ=="}'), 'test') } - let(:event_rb_segment_update) { SplitIoClient::SSE::EventSource::StreamData.new("data", 12345, JSON.parse('{"type":"RB_SEGMENT_UPDATE","changeNumber":5564531221,"pcn":1234,"c":0,"d":"eyJjaGFuZ2VOdW1iZXIiOiA1LCAibmFtZSI6ICJzYW1wbGVfcnVsZV9iYXNlZF9zZWdtZW50IiwgInN0YXR1cyI6ICJBQ1RJVkUiLCAidHJhZmZpY1R5cGVOYW1lIjogInVzZXIiLCAiZXhjbHVkZWQiOiB7ImtleXMiOiBbIm1hdXJvQHNwbGl0LmlvIl0sICJzZWdtZW50cyI6IFtdfSwgImNvbmRpdGlvbnMiOiBbeyJtYXRjaGVyR3JvdXAiOiB7ImNvbWJpbmVyIjogIkFORCIsICJtYXRjaGVycyI6IFt7ImtleVNlbGVjdG9yIjogeyJ0cmFmZmljVHlwZSI6ICJ1c2VyIiwgImF0dHJpYnV0ZSI6ICJlbWFpbCJ9LCAibWF0Y2hlclR5cGUiOiAiRU5EU19XSVRIIiwgIm5lZ2F0ZSI6IGZhbHNlLCAid2hpdGVsaXN0TWF0Y2hlckRhdGEiOiB7IndoaXRlbGlzdCI6IFsiQHNwbGl0LmlvIl19fV19fV19"}'), 'test') } + let(:event_rb_segment_update) { SplitIoClient::SSE::EventSource::StreamData.new("data", 12345, JSON.parse('{"type":"RB_SEGMENT_UPDATE","changeNumber":5564531221,"pcn":1234,"c":0,"d":"eyJjaGFuZ2VOdW1iZXIiOiA1LCAibmFtZSI6ICJzYW1wbGVfcnVsZV9iYXNlZF9zZWdtZW50IiwgInN0YXR1cyI6ICJBQ1RJVkUiLCAidHJhZmZpY1R5cGVOYW1lIjogInVzZXIiLCAiZXhjbHVkZWQiOiB7ImtleXMiOiBbIm1hdXJvQHNwbGl0LmlvIl0sICJzZWdtZW50cyI6IFt7InR5cGUiOiAic3RhbmRhcmQiLCAibmFtZSI6ICJzZWdtZW50MSJ9XX0sICJjb25kaXRpb25zIjogW3sibWF0Y2hlckdyb3VwIjogeyJjb21iaW5lciI6ICJBTkQiLCAibWF0Y2hlcnMiOiBbeyJrZXlTZWxlY3RvciI6IHsidHJhZmZpY1R5cGUiOiAidXNlciIsICJhdHRyaWJ1dGUiOiAiZW1haWwifSwgIm1hdGNoZXJUeXBlIjogIklOX1NFR01FTlQiLCAibmVnYXRlIjogZmFsc2UsICJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6IHsic2VnbWVudE5hbWUiOiAiZGVtbyJ9fV19fV19"}'), 'test') } context 'add change number to queue' do let(:flag_sets_repository) {SplitIoClient::Cache::Repositories::MemoryFlagSetsRepository.new([])} @@ -309,7 +309,7 @@ "trafficTypeName": "user", "excluded":{ "keys":["mauro@split.io","gaston@split.io"], - "segments":[] + "segments":[{ "type": "standard", "name": "segment1"}] }, "conditions": [ { @@ -321,13 +321,11 @@ "trafficType": "user", "attribute": "email" }, - "matcherType": "ENDS_WITH", + "matcherType": "IN_SEGMENT", "negate": false, - "whitelistMatcherData": { - "whitelist": [ - "@split.io" - ] - } + "userDefinedSegmentMatcherData":{ + "segmentName":"demo" + }, } ] } @@ -358,6 +356,8 @@ end it 'process rb segment update' do + stub_request(:get, 'https://sdk.split.io/api/segmentChanges/segment1?since=-1').to_return(status: 200, body: '{"name":"maur-2","added":["admin"],"removed":[],"since":-1,"till":-1}}') + stub_request(:get, 'https://sdk.split.io/api/segmentChanges/demo?since=-1').to_return(status: 200, body: '{"name":"maur-2","added":["admin"],"removed":[],"since":-1,"till":-1}}') worker = subject.new(synchronizer, config, splits_repository, telemetry_runtime_producer, segment_fetcher, rule_based_segments_repository) worker.start @@ -366,6 +366,11 @@ sleep 2 rb_segment = rule_based_segments_repository.get_rule_based_segment("sample_rule_based_segment") expect(rb_segment[:name] == 'sample_rule_based_segment') + + expect(a_request(:get, 'https://sdk.split.io/api/segmentChanges/demo?since=-1')).to have_been_made.once + expect(segments_repository.used_segment_names[0]).to eq('demo') + expect(a_request(:get, 'https://sdk.split.io/api/segmentChanges/segment1?since=-1')).to have_been_made.once + expect(segments_repository.used_segment_names[1]).to eq('segment1') end end diff --git a/spec/test_data/rule_based_segments/split_old_spec.json b/spec/test_data/rule_based_segments/split_old_spec.json index 0d7edf86..2d0aef1e 100644 --- a/spec/test_data/rule_based_segments/split_old_spec.json +++ b/spec/test_data/rule_based_segments/split_old_spec.json @@ -141,70 +141,6 @@ ], "sets": ["set3"] }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "sample_feature", - "seed": 1548363147, - "status": "ACTIVE", - "killed": false, - "changeNumber": 123, - "defaultTreatment": "off", - "configurations": { - "on": "{\"size\":15,\"test\":20}" - }, - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "employees" - }, - "whitelistMatcherData": null - } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - }, - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "human_beigns" - }, - "whitelistMatcherData": null - } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 30 - }, - { - "treatment": "off", - "size": 70 - } - ] - } - ], - "sets": ["set1"] - }, { "orgId": null, "environment": null, diff --git a/spec/test_data/rule_based_segments/split_old_spec2.json b/spec/test_data/rule_based_segments/split_old_spec2.json new file mode 100644 index 00000000..74aff1ed --- /dev/null +++ b/spec/test_data/rule_based_segments/split_old_spec2.json @@ -0,0 +1,66 @@ +{ + "splits": [ + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "new_feature", + "seed": -1222652054, + "status": "ACTIVE", + "killed": false, + "changeNumber": 123, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "whitelisted_user" + ] + } + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ] + }, + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ] + } + ], + "sets": ["set1", "set2"] + } ], + "since": -1, + "till": 1457726098069 +} \ No newline at end of file