Skip to content

Commit a82cf74

Browse files
authored
Merge pull request #559 from splitio/feature/rule-based-segments
Feature/rule based segments
2 parents 20368af + 4d4c830 commit a82cf74

File tree

111 files changed

+2686
-586
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+2686
-586
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
CHANGES
22

3+
8.6.0 (Jun xx, 2025)
4+
- Added support for rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK.
5+
- Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules.
6+
37
8.5.0 (Jan 17, 2025)
48
- Fixed high cpu usage when unique keys are cleared every 24 hours.
59
- Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on SplitView type objects. Read more in our docs.

lib/splitclient-rb.rb

Lines changed: 6 additions & 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'
@@ -47,6 +48,7 @@
4748
require 'splitclient-rb/helpers/decryption_helper'
4849
require 'splitclient-rb/helpers/util'
4950
require 'splitclient-rb/helpers/repository_helper'
51+
require 'splitclient-rb/helpers/evaluator_helper'
5052
require 'splitclient-rb/split_factory'
5153
require 'splitclient-rb/split_factory_builder'
5254
require 'splitclient-rb/split_config'
@@ -96,13 +98,17 @@
9698
require 'splitclient-rb/engine/matchers/less_than_or_equal_to_semver_matcher'
9799
require 'splitclient-rb/engine/matchers/between_semver_matcher'
98100
require 'splitclient-rb/engine/matchers/in_list_semver_matcher'
101+
require 'splitclient-rb/engine/matchers/rule_based_segment_matcher'
102+
require 'splitclient-rb/engine/matchers/prerequisites_matcher'
99103
require 'splitclient-rb/engine/evaluator/splitter'
100104
require 'splitclient-rb/engine/impressions/noop_unique_keys_tracker'
101105
require 'splitclient-rb/engine/impressions/unique_keys_tracker'
102106
require 'splitclient-rb/engine/metrics/binary_search_latency_tracker'
103107
require 'splitclient-rb/engine/models/split'
104108
require 'splitclient-rb/engine/models/label'
109+
require 'splitclient-rb/engine/models/segment_type'
105110
require 'splitclient-rb/engine/models/treatment'
111+
require 'splitclient-rb/engine/models/split_http_response'
106112
require 'splitclient-rb/engine/auth_api_client'
107113
require 'splitclient-rb/engine/back_off'
108114
require 'splitclient-rb/engine/push_manager'

lib/splitclient-rb/cache/fetchers/split_fetcher.rb

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ module SplitIoClient
22
module Cache
33
module Fetchers
44
class SplitFetcher
5-
attr_reader :splits_repository
5+
attr_reader :splits_repository, :rule_based_segments_repository
66

7-
def initialize(splits_repository, api_key, config, telemetry_runtime_producer)
7+
def initialize(splits_repository, rule_based_segments_repository, api_key, config, telemetry_runtime_producer)
88
@splits_repository = splits_repository
9+
@rule_based_segments_repository = rule_based_segments_repository
910
@api_key = api_key
1011
@config = config
1112
@semaphore = Mutex.new
@@ -23,10 +24,11 @@ def call
2324

2425
def fetch_splits(fetch_options = { cache_control_headers: false, till: nil })
2526
@semaphore.synchronize do
26-
data = splits_since(@splits_repository.get_change_number, fetch_options)
27-
28-
SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(@splits_repository, data[:splits], data[:till], @config)
27+
data = splits_since(@splits_repository.get_change_number, @rule_based_segments_repository.get_change_number, fetch_options)
28+
SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(@splits_repository, data[:ff][:d], data[:ff][:t], @config, @splits_api.clear_storage)
29+
SplitIoClient::Helpers::RepositoryHelper.update_rule_based_segment_repository(@rule_based_segments_repository, data[:rbs][:d], data[:rbs][:t], @config)
2930
@splits_repository.set_segment_names(data[:segment_names])
31+
@rule_based_segments_repository.set_segment_names(data[:segment_names])
3032
@config.logger.debug("segments seen(#{data[:segment_names].length}): #{data[:segment_names].to_a}") if @config.debug_enabled
3133

3234
{ segment_names: data[:segment_names], success: true }
@@ -55,8 +57,8 @@ def splits_thread
5557
end
5658
end
5759

58-
def splits_since(since, fetch_options = { cache_control_headers: false, till: nil })
59-
splits_api.since(since, fetch_options)
60+
def splits_since(since, since_rbs, fetch_options = { cache_control_headers: false, till: nil })
61+
splits_api.since(since, since_rbs, fetch_options)
6062
end
6163

6264
def splits_api
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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+
TILL_PREFIX = '.rbsegments.till'
28+
RB_SEGMENTS_PREFIX = '.rbsegment.'
29+
REGISTERED_PREFIX = '.segments.registered'
30+
31+
def initialize(config)
32+
super(config)
33+
@adapter = case @config.cache_adapter.class.to_s
34+
when 'SplitIoClient::Cache::Adapters::RedisAdapter'
35+
SplitIoClient::Cache::Adapters::CacheAdapter.new(@config)
36+
else
37+
@config.cache_adapter
38+
end
39+
unless @config.mode.equal?(:consumer)
40+
@adapter.set_string(namespace_key(TILL_PREFIX), '-1')
41+
@adapter.initialize_map(namespace_key(REGISTERED_PREFIX))
42+
end
43+
end
44+
45+
def update(to_add, to_delete, new_change_number)
46+
to_add.each{ |rule_based_segment| add_rule_based_segment(rule_based_segment) }
47+
to_delete.each{ |rule_based_segment| remove_rule_based_segment(rule_based_segment) }
48+
set_change_number(new_change_number)
49+
end
50+
51+
def get_rule_based_segment(name)
52+
rule_based_segment = @adapter.string(namespace_key("#{RB_SEGMENTS_PREFIX}#{name}"))
53+
54+
JSON.parse(rule_based_segment, symbolize_names: true) if rule_based_segment
55+
end
56+
57+
def rule_based_segment_names
58+
@adapter.find_strings_by_prefix(namespace_key(RB_SEGMENTS_PREFIX))
59+
.map { |rule_based_segment_names| rule_based_segment_names.gsub(namespace_key(RB_SEGMENTS_PREFIX), '') }
60+
end
61+
62+
def set_change_number(since)
63+
@adapter.set_string(namespace_key(TILL_PREFIX), since)
64+
end
65+
66+
def get_change_number
67+
@adapter.string(namespace_key(TILL_PREFIX))
68+
end
69+
70+
def set_segment_names(names)
71+
return if names.nil? || names.empty?
72+
73+
names.each do |name|
74+
@adapter.add_to_set(namespace_key(REGISTERED_PREFIX), name)
75+
end
76+
end
77+
78+
def exists?(name)
79+
@adapter.exists?(namespace_key("#{RB_SEGMENTS_PREFIX}#{name}"))
80+
end
81+
82+
def clear
83+
@adapter.clear(namespace_key)
84+
end
85+
86+
def contains?(segment_names)
87+
return false if rule_based_segment_names.empty?
88+
return set(segment_names).subset?(rule_based_segment_names)
89+
end
90+
91+
private
92+
93+
def add_rule_based_segment(rule_based_segment)
94+
return unless rule_based_segment[:name]
95+
96+
if check_undefined_matcher(rule_based_segment)
97+
@config.logger.warn("Rule based segment #{rule_based_segment[:name]} has undefined matcher, setting conditions to default template.")
98+
rule_based_segment[:conditions] = RuleBasedSegmentsRepository::DEFAULT_CONDITIONS_TEMPLATE
99+
end
100+
101+
@adapter.set_string(namespace_key("#{RB_SEGMENTS_PREFIX}#{rule_based_segment[:name]}"), rule_based_segment.to_json)
102+
end
103+
104+
def check_undefined_matcher(rule_based_segment)
105+
for condition in rule_based_segment[:conditions]
106+
for matcher in condition[:matcherGroup][:matchers]
107+
if !SplitIoClient::Condition.instance_methods(false).map(&:to_s).include?("matcher_#{matcher[:matcherType].downcase}")
108+
@config.logger.error("Detected undefined matcher #{matcher[:matcherType].downcase} in feature flag #{rule_based_segment[:name]}")
109+
return true
110+
end
111+
end
112+
end
113+
return false
114+
end
115+
116+
def remove_rule_based_segment(rule_based_segment)
117+
@adapter.delete(namespace_key("#{RB_SEGMENTS_PREFIX}#{rule_based_segment[:name]}"))
118+
end
119+
end
120+
end
121+
end
122+
end

lib/splitclient-rb/cache/repositories/segments_repository.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ def segment_keys_count
8383
0
8484
end
8585

86+
def contains?(segment_names)
87+
if segment_names.empty?
88+
return false
89+
end
90+
return segment_names.to_set.subset?(used_segment_names.to_set)
91+
end
92+
8693
private
8794

8895
def segment_data(name)

lib/splitclient-rb/cache/repositories/splits_repository.rb

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class SplitsRepository < Repository
3131
],
3232
label: "targeting rule type unsupported by sdk"
3333
}]
34+
TILL_PREFIX = '.splits.till'
35+
SPLIT_PREFIX = '.split.'
36+
READY_PREFIX = '.splits.ready'
3437

3538
def initialize(config, flag_sets_repository, flag_set_filter)
3639
super(config)
@@ -43,10 +46,7 @@ def initialize(config, flag_sets_repository, flag_set_filter)
4346
end
4447
@flag_sets = flag_sets_repository
4548
@flag_set_filter = flag_set_filter
46-
unless @config.mode.equal?(:consumer)
47-
@adapter.set_string(namespace_key('.splits.till'), '-1')
48-
@adapter.initialize_map(namespace_key('.segments.registered'))
49-
end
49+
initialize_keys
5050
end
5151

5252
def update(to_add, to_delete, new_change_number)
@@ -87,16 +87,16 @@ def traffic_type_exists(tt_name)
8787

8888
# Return an array of Split Names excluding control keys like splits.till
8989
def split_names
90-
@adapter.find_strings_by_prefix(namespace_key('.split.'))
91-
.map { |split| split.gsub(namespace_key('.split.'), '') }
90+
@adapter.find_strings_by_prefix(namespace_key(SPLIT_PREFIX))
91+
.map { |split| split.gsub(namespace_key(SPLIT_PREFIX), '') }
9292
end
9393

9494
def set_change_number(since)
95-
@adapter.set_string(namespace_key('.splits.till'), since)
95+
@adapter.set_string(namespace_key(TILL_PREFIX), since)
9696
end
9797

9898
def get_change_number
99-
@adapter.string(namespace_key('.splits.till'))
99+
@adapter.string(namespace_key(TILL_PREFIX))
100100
end
101101

102102
def set_segment_names(names)
@@ -112,21 +112,22 @@ def exists?(name)
112112
end
113113

114114
def ready?
115-
@adapter.string(namespace_key('.splits.ready')).to_i != -1
115+
@adapter.string(namespace_key(READY_PREFIX)).to_i != -1
116116
end
117117

118118
def not_ready!
119-
@adapter.set_string(namespace_key('.splits.ready'), -1)
119+
@adapter.set_string(namespace_key(READY_PREFIX), -1)
120120
end
121121

122122
def ready!
123-
@adapter.set_string(namespace_key('.splits.ready'), Time.now.utc.to_i)
123+
@adapter.set_string(namespace_key(READY_PREFIX), Time.now.utc.to_i)
124124
end
125125

126126
def clear
127127
@tt_cache.clear
128128

129129
@adapter.clear(namespace_key)
130+
initialize_keys
130131
end
131132

132133
def kill(change_number, split_name, default_treatment)
@@ -167,6 +168,13 @@ def flag_set_filter
167168

168169
private
169170

171+
def initialize_keys
172+
unless @config.mode.equal?(:consumer)
173+
@adapter.set_string(namespace_key(TILL_PREFIX), '-1')
174+
@adapter.initialize_map(namespace_key('.segments.registered'))
175+
end
176+
end
177+
170178
def add_feature_flag(split)
171179
return unless split[:name]
172180
existing_split = get_split(split[:name])

lib/splitclient-rb/cache/stores/localhost_split_builder.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ def build_split(feature, treatments)
2222
seed: 2_089_907_429,
2323
defaultTreatment: 'control_treatment',
2424
configurations: build_configurations(treatments),
25-
conditions: build_conditions(treatments)
25+
conditions: build_conditions(treatments),
26+
prerequisites: []
2627
}
2728
end
2829

lib/splitclient-rb/clients/split_client.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def initialize(sdk_key, repositories, status_manager, config, impressions_manage
2222
@api_key = sdk_key
2323
@splits_repository = repositories[:splits]
2424
@segments_repository = repositories[:segments]
25+
@rule_based_segments_repository = repositories[:rule_based_segments]
2526
@impressions_repository = repositories[:impressions]
2627
@events_repository = repositories[:events]
2728
@status_manager = status_manager
@@ -115,6 +116,7 @@ def destroy
115116

116117
@splits_repository.clear
117118
@segments_repository.clear
119+
@rule_based_segments_repository.clear
118120

119121
SplitIoClient.load_factory_registry
120122
SplitIoClient.split_factory_registry.remove(@api_key)

lib/splitclient-rb/engine/api/client.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ def get_api(url, api_key, params = {}, cache_control_headers = false)
2020

2121
@config.split_logger.log_if_debug("GET #{url} proxy: #{api_client.proxy}")
2222
end
23+
24+
rescue Zlib::GzipFile::Error => e
25+
@config.logger.warn("Incorrect formatted response exception: #{e}\n")
26+
SplitIoClient::Engine::Models::SplitHttpResponse.new(400, '', true)
27+
2328
rescue StandardError => e
2429
@config.logger.warn("#{e}\nURL:#{url}\nparams:#{params}")
2530
raise e, 'Split SDK failed to connect to backend to retrieve information', e.backtrace
@@ -50,6 +55,9 @@ def post_api(url, api_key, data, headers = {}, params = {})
5055
raise e, 'Split SDK failed to connect to backend to post information', e.backtrace
5156
end
5257

58+
def sdk_url_overriden?
59+
@config.sdk_url_overriden?
60+
end
5361
private
5462

5563
def api_client

0 commit comments

Comments
 (0)