From 41651aefdd7d3918bb7fae77b256471dd568b944 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 26 Oct 2023 16:00:23 -0700 Subject: [PATCH] added flagset to split cach repository --- lib/splitclient-rb.rb | 2 + .../cache/filter/flag_set_filter.rb | 40 +++++++++++++ .../repositories/flag_sets_repository.rb | 38 +++++++++++++ .../cache/repositories/splits_repository.rb | 49 +++++++++++++++- .../repositories/splits_repository_spec.rb | 56 ++++++++++++++++++- 5 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 lib/splitclient-rb/cache/filter/flag_set_filter.rb create mode 100644 lib/splitclient-rb/cache/repositories/flag_sets_repository.rb diff --git a/lib/splitclient-rb.rb b/lib/splitclient-rb.rb index bba7eb84..5eb33d2d 100644 --- a/lib/splitclient-rb.rb +++ b/lib/splitclient-rb.rb @@ -14,6 +14,7 @@ require 'splitclient-rb/cache/fetchers/split_fetcher' require 'splitclient-rb/cache/filter/bloom_filter' require 'splitclient-rb/cache/filter/filter_adapter' +require 'splitclient-rb/cache/filter/flag_set_filter' require 'splitclient-rb/cache/hashers/impression_hasher' require 'splitclient-rb/cache/observers/impression_observer' require 'splitclient-rb/cache/observers/noop_impression_observer' @@ -24,6 +25,7 @@ require 'splitclient-rb/cache/repositories/impressions_repository' require 'splitclient-rb/cache/repositories/events/memory_repository' require 'splitclient-rb/cache/repositories/events/redis_repository' +require 'splitclient-rb/cache/repositories/flag_sets_repository' require 'splitclient-rb/cache/repositories/impressions/memory_repository' require 'splitclient-rb/cache/repositories/impressions/redis_repository' require 'splitclient-rb/cache/senders/impressions_formatter' diff --git a/lib/splitclient-rb/cache/filter/flag_set_filter.rb b/lib/splitclient-rb/cache/filter/flag_set_filter.rb new file mode 100644 index 00000000..09f25d66 --- /dev/null +++ b/lib/splitclient-rb/cache/filter/flag_set_filter.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'set' + +module SplitIoClient + module Cache + module Filter + class FlagSetsFilter + def initialize(flag_sets = []) + @flag_sets = Set.new(flag_sets) + @should_filter = @flag_sets.any? + end + + def should_filter? + @should_filter + end + + def flag_set_exist?(flag_set) + return true unless @should_filter + + if not flag_set.is_a?(String) or flag_set.empty? + return false + end + + @flag_sets.intersection([flag_set]).any? + end + + def intersect?(flag_sets) + return true unless @should_filter + + if not flag_sets.is_a?(Array) or flag_sets.empty? + return false + end + + @flag_sets.intersection(Set.new(flag_sets)).any? + end + end + end + end +end diff --git a/lib/splitclient-rb/cache/repositories/flag_sets_repository.rb b/lib/splitclient-rb/cache/repositories/flag_sets_repository.rb new file mode 100644 index 00000000..70b0c8f5 --- /dev/null +++ b/lib/splitclient-rb/cache/repositories/flag_sets_repository.rb @@ -0,0 +1,38 @@ +require 'concurrent' + +module SplitIoClient + module Cache + module Repositories + class FlagSets + def initialize(flag_sets = []) + @sets_feature_flag_map = {} + flag_sets.each{ |flag_set| @sets_feature_flag_map[flag_set] = Set[] } + end + + def flag_set_exist?(flag_set) + @sets_feature_flag_map.keys.include? flag_set + end + + def get_flag_set(flag_set) + @sets_feature_flag_map[flag_set] + end + + def add_flag_set(flag_set) + @sets_feature_flag_map[flag_set] = Set[] if !flag_set_exist?(flag_set) + end + + def remove_flag_set(flag_set) + @sets_feature_flag_map.delete(flag_set) if flag_set_exist?(flag_set) + end + + def add_feature_flag_to_flag_set(flag_set, feature_flag) + @sets_feature_flag_map[flag_set].add(feature_flag) if flag_set_exist?(flag_set) + end + + def remove_feature_flag_to_flag_set(flag_set, feature_flag) + @sets_feature_flag_map[flag_set].delete(feature_flag) if flag_set_exist?(flag_set) + end + end + end + end +end diff --git a/lib/splitclient-rb/cache/repositories/splits_repository.rb b/lib/splitclient-rb/cache/repositories/splits_repository.rb index 383704b1..6485d3ea 100644 --- a/lib/splitclient-rb/cache/repositories/splits_repository.rb +++ b/lib/splitclient-rb/cache/repositories/splits_repository.rb @@ -6,7 +6,7 @@ module Repositories class SplitsRepository < Repository attr_reader :adapter - def initialize(config) + def initialize(config, flag_sets = []) super(config) @tt_cache = {} @adapter = case @config.cache_adapter.class.to_s @@ -15,6 +15,8 @@ def initialize(config) else @config.cache_adapter end + @flag_sets = SplitIoClient::Cache::Repositories::FlagSets.new(flag_sets) + @flag_set_filter = SplitIoClient::Cache::Filter::FlagSetsFilter.new(flag_sets) unless @config.mode.equal?(:consumer) @adapter.set_string(namespace_key('.splits.till'), '-1') @adapter.initialize_map(namespace_key('.segments.registered')) @@ -30,6 +32,19 @@ def add_split(split) elsif(existing_split[:trafficTypeName] != split[:trafficTypeName]) increase_tt_name_count(split[:trafficTypeName]) decrease_tt_name_count(existing_split[:trafficTypeName]) + remove_from_flag_sets(existing_split) + end + + if !split[:sets].nil? + for flag_set in split[:sets] + if !@flag_sets.flag_set_exist?(flag_set) + if @flag_set_filter.should_filter? + next + end + @flag_sets.add_flag_set(flag_set) + end + @flag_sets.add_feature_flag_to_flag_set(flag_set, split[:name]) + end end @adapter.set_string(namespace_key(".split.#{split[:name]}"), split.to_json) @@ -39,7 +54,7 @@ def remove_split(split) tt_name = split[:trafficTypeName] decrease_tt_name_count(split[:trafficTypeName]) - + remove_from_flag_sets(split) @adapter.delete(namespace_key(".split.#{split[:name]}")) end @@ -144,8 +159,38 @@ def splits_count split_names.length end + def get_feature_flags_by_sets(flag_sets) + sets_to_fetch = Array.new + for flag_set in flag_sets + if !@flag_sets.flag_set_exist?(flag_set) + @config.logger.warn("Flag set #{flag_set} is not part of the configured flag set list, ignoring it.") + next + end + sets_to_fetch.push(flag_set) + end + + to_return = Array.new + sets_to_fetch.each { |flag_set| to_return.concat(@flag_sets.get_flag_set(flag_set).to_a)} + to_return.uniq + end + + def is_flag_set_exist(flag_set) + @flag_sets.flag_set_exist?(flag_set) + end + private + def remove_from_flag_sets(feature_flag) + if !feature_flag[:sets].nil? + for flag_set in feature_flag[:sets] + @flag_sets.remove_feature_flag_to_flag_set(flag_set, feature_flag[:name]) + if is_flag_set_exist(flag_set) && @flag_sets.get_flag_set(flag_set).length == 0 && !@flag_set_filter.should_filter? + @flag_sets.remove_flag_set(flag_set) + end + end + end + end + def increase_tt_name_count(tt_name) return unless tt_name diff --git a/spec/cache/repositories/splits_repository_spec.rb b/spec/cache/repositories/splits_repository_spec.rb index 75d4c84f..11b0b2f4 100644 --- a/spec/cache/repositories/splits_repository_spec.rb +++ b/spec/cache/repositories/splits_repository_spec.rb @@ -6,7 +6,8 @@ describe SplitIoClient::Cache::Repositories::SplitsRepository do RSpec.shared_examples 'Splits Repository' do |cache_adapter| let(:config) { SplitIoClient::SplitConfig.new(cache_adapter: cache_adapter) } - let(:repository) { described_class.new(config) } + let(:repository) { described_class.new(config, []) } + let(:repository2) { described_class.new(config, ['set_1', 'set_2']) } before :all do redis = Redis.new @@ -101,6 +102,59 @@ 'baz' => { name: 'baz', trafficTypeName: 'tt_name_1' } ) end + + it 'check without flagset filter' do + repository.add_split(name: 'split1', trafficTypeName: 'tt_name_1', sets: ['set_1']) + repository.add_split(name: 'split2', trafficTypeName: 'tt_name_2') + repository.add_split(name: 'split3', trafficTypeName: 'tt_name_1', sets: ['set_2']) + + expect(repository.is_flag_set_exist('set_1')).to be true + expect(repository.is_flag_set_exist('set_2')).to be true + expect(repository.is_flag_set_exist('set_3')).to be false + expect(repository.get_feature_flags_by_sets(['set_3'])).to eq([]) + expect(repository.get_feature_flags_by_sets(['set_1'])).to eq(['split1']) + expect(repository.get_feature_flags_by_sets(['set_2'])).to eq(['split3']) + expect(repository.get_feature_flags_by_sets(['set_2', 'set_1']).sort).to eq(['split1', 'split3']) + + repository.add_split(name: 'split4', trafficTypeName: 'tt_name_1', sets: ['set_1']) + expect(repository.get_feature_flags_by_sets(['set_1'])).to eq(['split1', 'split4']) + + repository.remove_split(name: 'split1', trafficTypeName: 'tt_name_1', sets: ['set_1']) + expect(repository.is_flag_set_exist('set_1')).to be true + expect(repository.get_feature_flags_by_sets(['set_1'])).to eq(['split4']) + + repository.remove_split(name: 'split4', trafficTypeName: 'tt_name_1', sets: ['set_1']) + expect(repository.is_flag_set_exist('set_1')).to be false + expect(repository.get_feature_flags_by_sets(['set_1'])).to eq([]) + expect(repository.get_feature_flags_by_sets(['set_2', 'set_1']).sort).to eq(['split3']) + end + + it 'check with flagset filter' do + repository2.add_split(name: 'split1', trafficTypeName: 'tt_name_1', sets: ['set_1']) + repository2.add_split(name: 'split2', trafficTypeName: 'tt_name_2', sets: ['set_3']) + repository2.add_split(name: 'split3', trafficTypeName: 'tt_name_1', sets: ['set_2']) + + expect(repository2.is_flag_set_exist('set_1')).to be true + expect(repository2.is_flag_set_exist('set_2')).to be true + expect(repository2.is_flag_set_exist('set_3')).to be false + expect(repository2.get_feature_flags_by_sets(['set_3'])).to eq([]) + expect(repository2.get_feature_flags_by_sets(['set_1'])).to eq(['split1']) + expect(repository2.get_feature_flags_by_sets(['set_2'])).to eq(['split3']) + expect(repository2.get_feature_flags_by_sets(['set_2', 'set_1']).sort).to eq(['split1', 'split3']) + + repository2.add_split(name: 'split4', trafficTypeName: 'tt_name_1', sets: ['set_1']) + expect(repository2.get_feature_flags_by_sets(['set_1'])).to eq(['split1', 'split4']) + + repository2.remove_split(name: 'split1', trafficTypeName: 'tt_name_1', sets: ['set_1']) + expect(repository2.is_flag_set_exist('set_1')).to be true + expect(repository2.get_feature_flags_by_sets(['set_1'])).to eq(['split4']) + + repository2.remove_split(name: 'split4', trafficTypeName: 'tt_name_1', sets: ['set_1']) + expect(repository2.is_flag_set_exist('set_1')).to be true + expect(repository2.get_feature_flags_by_sets(['set_1'])).to eq([]) + expect(repository2.get_feature_flags_by_sets(['set_2', 'set_1']).sort).to eq(['split3']) + end + end describe 'with Memory Adapter' do