Skip to content

Commit a7b618a

Browse files
authored
Merge pull request #547 from splitio/rbs-cache
Added rbs cache class
2 parents f6360df + 62eae92 commit a7b618a

File tree

3 files changed

+229
-0
lines changed

3 files changed

+229
-0
lines changed

lib/splitclient-rb.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
require 'splitclient-rb/cache/repositories/splits_repository'
2424
require 'splitclient-rb/cache/repositories/events_repository'
2525
require 'splitclient-rb/cache/repositories/impressions_repository'
26+
require 'splitclient-rb/cache/repositories/rule_based_segments_repository'
2627
require 'splitclient-rb/cache/repositories/events/memory_repository'
2728
require 'splitclient-rb/cache/repositories/events/redis_repository'
2829
require 'splitclient-rb/cache/repositories/flag_sets/memory_repository'
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
require 'concurrent'
2+
3+
module SplitIoClient
4+
module Cache
5+
module Repositories
6+
class RuleBasedSegmentsRepository < Repository
7+
attr_reader :adapter
8+
DEFAULT_CONDITIONS_TEMPLATE = [{
9+
conditionType: "ROLLOUT",
10+
matcherGroup: {
11+
combiner: "AND",
12+
matchers: [
13+
{
14+
keySelector: nil,
15+
matcherType: "ALL_KEYS",
16+
negate: false,
17+
userDefinedSegmentMatcherData: nil,
18+
whitelistMatcherData: nil,
19+
unaryNumericMatcherData: nil,
20+
betweenMatcherData: nil,
21+
dependencyMatcherData: nil,
22+
booleanMatcherData: nil,
23+
stringMatcherData: nil
24+
}]
25+
}
26+
}]
27+
28+
def initialize(config)
29+
super(config)
30+
@adapter = case @config.cache_adapter.class.to_s
31+
when 'SplitIoClient::Cache::Adapters::RedisAdapter'
32+
SplitIoClient::Cache::Adapters::CacheAdapter.new(@config)
33+
else
34+
@config.cache_adapter
35+
end
36+
unless @config.mode.equal?(:consumer)
37+
@adapter.set_string(namespace_key('.rbsegments.till'), '-1')
38+
@adapter.initialize_map(namespace_key('.segments.registered'))
39+
end
40+
end
41+
42+
def update(to_add, to_delete, new_change_number)
43+
to_add.each{ |rule_based_segment| add_rule_based_segment(rule_based_segment) }
44+
to_delete.each{ |rule_based_segment| remove_rule_based_segment(rule_based_segment) }
45+
set_change_number(new_change_number)
46+
end
47+
48+
def get_rule_based_segment(name)
49+
rule_based_segment = @adapter.string(namespace_key(".rbsegment.#{name}"))
50+
51+
JSON.parse(rule_based_segment, symbolize_names: true) if rule_based_segment
52+
end
53+
54+
def rule_based_segment_names
55+
@adapter.find_strings_by_prefix(namespace_key('.rbsegment.'))
56+
.map { |rule_based_segment_names| rule_based_segment_names.gsub(namespace_key('.rbsegment.'), '') }
57+
end
58+
59+
def set_change_number(since)
60+
@adapter.set_string(namespace_key('.rbsegments.till'), since)
61+
end
62+
63+
def get_change_number
64+
@adapter.string(namespace_key('.rbsegments.till'))
65+
end
66+
67+
def set_segment_names(names)
68+
return if names.nil? || names.empty?
69+
70+
names.each do |name|
71+
@adapter.add_to_set(namespace_key('.segments.registered'), name)
72+
end
73+
end
74+
75+
def exists?(name)
76+
@adapter.exists?(namespace_key(".rbsegment.#{name}"))
77+
end
78+
79+
def clear
80+
@adapter.clear(namespace_key)
81+
end
82+
83+
def contains?(segment_names)
84+
return set(segment_names).subset?(rule_based_segment_names)
85+
end
86+
87+
private
88+
89+
def add_rule_based_segment(rule_based_segment)
90+
return unless rule_based_segment[:name]
91+
existing_rule_based_segment = get_rule_based_segment(rule_based_segment[:name])
92+
93+
if check_undefined_matcher(rule_based_segment)
94+
@config.logger.warn("Rule based segment #{rule_based_segment[:name]} has undefined matcher, setting conditions to default template.")
95+
rule_based_segment[:conditions] = RuleBasedSegmentsRepository::DEFAULT_CONDITIONS_TEMPLATE
96+
end
97+
98+
@adapter.set_string(namespace_key(".rbsegment.#{rule_based_segment[:name]}"), rule_based_segment.to_json)
99+
end
100+
101+
def check_undefined_matcher(rule_based_segment)
102+
for condition in rule_based_segment[:conditions]
103+
for matcher in condition[:matcherGroup][:matchers]
104+
if !SplitIoClient::Condition.instance_methods(false).map(&:to_s).include?("matcher_#{matcher[:matcherType].downcase}")
105+
@config.logger.error("Detected undefined matcher #{matcher[:matcherType].downcase} in feature flag #{rule_based_segment[:name]}")
106+
return true
107+
end
108+
end
109+
end
110+
return false
111+
end
112+
113+
def remove_rule_based_segment(rule_based_segment)
114+
@adapter.delete(namespace_key(".rbsegment.#{rule_based_segment[:name]}"))
115+
end
116+
end
117+
end
118+
end
119+
end
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'set'
5+
6+
describe SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository do
7+
RSpec.shared_examples 'RuleBasedSegments Repository' do |cache_adapter|
8+
let(:config) { SplitIoClient::SplitConfig.new(cache_adapter: cache_adapter) }
9+
let(:repository) { described_class.new(config) }
10+
11+
before :all do
12+
redis = Redis.new
13+
redis.flushall
14+
end
15+
16+
before do
17+
# in memory setup
18+
repository.update([{name: 'foo', trafficTypeName: 'tt_name_1', conditions: []},
19+
{name: 'bar', trafficTypeName: 'tt_name_2', conditions: []},
20+
{name: 'baz', trafficTypeName: 'tt_name_1', conditions: []}], [], -1)
21+
end
22+
23+
after do
24+
repository.update([], [{name: 'foo', trafficTypeName: 'tt_name_1', conditions: []},
25+
{name: 'bar', trafficTypeName: 'tt_name_2', conditions: []},
26+
{name: 'bar', trafficTypeName: 'tt_name_2', conditions: []},
27+
{name: 'qux', trafficTypeName: 'tt_name_3', conditions: []},
28+
{name: 'quux', trafficTypeName: 'tt_name_4', conditions: []},
29+
{name: 'corge', trafficTypeName: 'tt_name_5', conditions: []},
30+
{name: 'corge', trafficTypeName: 'tt_name_6', conditions: []}], -1)
31+
end
32+
33+
it 'returns rule_based_segments names' do
34+
expect(Set.new(repository.rule_based_segment_names)).to eq(Set.new(%w[foo bar baz]))
35+
end
36+
37+
it 'returns rule_based_segment data' do
38+
expect(repository.get_rule_based_segment('foo')).to eq(
39+
{ conditions: [] , name: 'foo', trafficTypeName: 'tt_name_1' },
40+
)
41+
end
42+
43+
it 'remove undefined matcher with template condition' do
44+
rule_based_segment = { name: 'corge', trafficTypeName: 'tt_name_5', conditions: [
45+
{
46+
contitionType: 'WHITELIST',
47+
label: 'some_label',
48+
matcherGroup: {
49+
matchers: [
50+
{
51+
matcherType: 'UNDEFINED',
52+
whitelistMatcherData: {
53+
whitelist: ['k1', 'k2', 'k3']
54+
},
55+
negate: false,
56+
}
57+
],
58+
combiner: 'AND'
59+
}
60+
}]
61+
}
62+
63+
repository.update([rule_based_segment], [], -1)
64+
expect(repository.get_rule_based_segment('corge')[:conditions]).to eq SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository::DEFAULT_CONDITIONS_TEMPLATE
65+
66+
# test with multiple conditions
67+
rule_based_segment2 = {
68+
name: 'corge2',
69+
trafficTypeName: 'tt_name_5',
70+
conditions: [
71+
{
72+
contitionType: 'WHITELIST',
73+
label: 'some_label',
74+
matcherGroup: {
75+
matchers: [
76+
{
77+
matcherType: 'UNDEFINED',
78+
whitelistMatcherData: {
79+
whitelist: ['k1', 'k2', 'k3']
80+
},
81+
negate: false,
82+
}
83+
],
84+
combiner: 'AND'
85+
}
86+
},
87+
{
88+
contitionType: 'WHITELIST',
89+
label: 'some_other_label',
90+
matcherGroup: {
91+
matchers: [{matcherType: 'ALL_KEYS', negate: false}],
92+
combiner: 'AND'
93+
}
94+
}]
95+
}
96+
97+
repository.update([rule_based_segment2], [], -1)
98+
expect(repository.get_rule_based_segment('corge2')[:conditions]).to eq SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository::DEFAULT_CONDITIONS_TEMPLATE
99+
end
100+
end
101+
102+
describe 'with Memory Adapter' do
103+
it_behaves_like 'RuleBasedSegments Repository', :memory
104+
end
105+
106+
describe 'with Redis Adapter' do
107+
it_behaves_like 'RuleBasedSegments Repository', :redis
108+
end
109+
end

0 commit comments

Comments
 (0)