splitclient-rb 2.0.1 → 3.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.txt +12 -0
- data/NEWS +4 -0
- data/README.md +45 -11
- data/lib/cache/adapters/adapter.rb +23 -0
- data/lib/cache/adapters/memory_adapter.rb +46 -0
- data/lib/cache/repositories/repository.rb +25 -0
- data/lib/cache/repositories/segments_repository.rb +52 -0
- data/lib/cache/repositories/splits_repository.rb +51 -0
- data/lib/cache/stores/sdk_blocker.rb +47 -0
- data/lib/cache/stores/segment_store.rb +71 -0
- data/lib/cache/stores/split_store.rb +64 -0
- data/lib/engine/api/client.rb +29 -0
- data/lib/engine/api/segments.rb +60 -0
- data/lib/engine/api/splits.rb +58 -0
- data/lib/{splitclient-engine → engine}/evaluator/splitter.rb +0 -0
- data/lib/{splitclient-engine → engine}/impressions/impressions.rb +0 -0
- data/lib/{splitclient-engine → engine}/matchers/all_keys_matcher.rb +0 -0
- data/lib/{splitclient-engine → engine}/matchers/between_matcher.rb +2 -0
- data/lib/{splitclient-engine → engine}/matchers/combiners.rb +0 -0
- data/lib/{splitclient-engine → engine}/matchers/combining_matcher.rb +1 -1
- data/lib/{splitclient-engine → engine}/matchers/equal_to_matcher.rb +0 -0
- data/lib/{splitclient-engine → engine}/matchers/greater_than_or_equal_to_matcher.rb +0 -0
- data/lib/{splitclient-engine → engine}/matchers/less_than_or_equal_to_matcher.rb +0 -0
- data/lib/{splitclient-engine → engine}/matchers/negation_matcher.rb +0 -0
- data/lib/{splitclient-engine → engine}/matchers/user_defined_segment_matcher.rb +4 -21
- data/lib/{splitclient-engine → engine}/matchers/whitelist_matcher.rb +0 -0
- data/lib/{splitclient-engine → engine}/metrics/binary_search_latency_tracker.rb +0 -0
- data/lib/{splitclient-engine → engine}/metrics/metrics.rb +0 -0
- data/lib/{splitclient-engine → engine}/parser/condition.rb +5 -7
- data/lib/{splitclient-engine → engine}/parser/partition.rb +0 -0
- data/lib/{splitclient-engine → engine}/parser/split.rb +11 -3
- data/lib/{splitclient-engine → engine}/parser/split_adapter.rb +20 -184
- data/lib/engine/parser/split_treatment.rb +65 -0
- data/lib/{splitclient-engine → engine}/partitions/treatments.rb +0 -0
- data/lib/exceptions/sdk_blocker_timeout_expired_exception.rb +4 -0
- data/lib/splitclient-rb.rb +31 -23
- data/lib/splitclient-rb/split_config.rb +41 -4
- data/lib/splitclient-rb/split_factory.rb +50 -20
- data/lib/splitclient-rb/version.rb +1 -1
- data/splitclient-rb.gemspec +2 -0
- metadata +62 -25
- data/lib/splitclient-cache/local_store.rb +0 -45
- data/lib/splitclient-engine/parser/segment.rb +0 -84
- data/lib/splitclient-engine/parser/segment_parser.rb +0 -46
- data/lib/splitclient-engine/parser/split_parser.rb +0 -122
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae8492b8f79d39cda6b4c52bb8300c8d7374fef4
|
4
|
+
data.tar.gz: '09129f27b81bd42cea7731786f62b4bffdbc5c96'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b5595aecddeef4d878fd02f7748b0e8a97cfe9870495f8e0fee0198ec7db71673c379f995154e80f1f6cc34a6c411c047f6cacf76a3204d4c9681baf566687bd
|
7
|
+
data.tar.gz: fc242d04f2fd5fb1590239d212da8e427a47cc9f0d2cd069c107b84d78fa64e47d10e05049b81e54bd4a8c00503f47885c4238b8c89c9818b1b493c002b1b3e0
|
data/CHANGES.txt
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
3.0.2
|
2
|
+
- add ability to provide different bucketing/matching keys
|
3
|
+
|
4
|
+
3.0.1
|
5
|
+
- fix segments not deleting from the cache
|
6
|
+
|
7
|
+
3.0.0
|
8
|
+
- add new caching interface
|
9
|
+
- add replaceable adapters to store cache in
|
10
|
+
- add first cache adapter: MemoryAdapter
|
11
|
+
- refactoring
|
12
|
+
|
1
13
|
2.0.1
|
2
14
|
- Supress warnings cause by Net::HTTP when it already exists.
|
3
15
|
|
data/NEWS
CHANGED
data/README.md
CHANGED
@@ -100,23 +100,40 @@ The following values can be customized
|
|
100
100
|
**impressions_refresh_rate** : The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers in seconds
|
101
101
|
*default value* = 60
|
102
102
|
|
103
|
+
**debug_enabled** : Enables extra logging
|
104
|
+
*default value* = false
|
105
|
+
|
106
|
+
**transport_debug_enabled** : Enables extra transport logging
|
107
|
+
*default value* = false
|
108
|
+
|
103
109
|
**logger** : default logger for messages and errors
|
104
110
|
*default value* : Ruby logger class set to STDOUT
|
105
111
|
|
112
|
+
**block_until_ready** : The SDK will block your app for provided amount of seconds until it's ready. If timeout expires `SplitIoClient::SDKBlockerTimeoutExpiredException` will be thrown. If `false` provided, then SDK would run in non-blocking mode
|
113
|
+
*default value* : false
|
114
|
+
|
106
115
|
Example
|
107
116
|
```ruby
|
108
|
-
options = {
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
117
|
+
options = {
|
118
|
+
base_uri: 'https://my.app.api/',
|
119
|
+
local_store: Rails.cache,
|
120
|
+
connection_timeout: 10,
|
121
|
+
read_timeout: 5,
|
122
|
+
features_refresh_rate: 120,
|
123
|
+
segments_refresh_rate: 120,
|
124
|
+
metrics_refresh_rate: 360,
|
125
|
+
impressions_refresh_rate: 360,
|
126
|
+
logger: Logger.new('logfile.log'),
|
127
|
+
block_until_ready: 5
|
128
|
+
}
|
129
|
+
begin
|
130
|
+
split_client = SplitIoClient::SplitFactory.new("your_api_key", options).client
|
131
|
+
rescue SplitIoClient::SDKBlockerTimeoutExpiredException
|
132
|
+
# Some arbitrary actions
|
133
|
+
end
|
119
134
|
```
|
135
|
+
This begin-rescue-end block is optional, you might want to use it to catch timeout expired exception and apply some logic here.
|
136
|
+
|
120
137
|
### Execution
|
121
138
|
---
|
122
139
|
In your application code you just need to call the get_treatment method with the required parameters for key and feature name
|
@@ -131,6 +148,23 @@ if split_client.get_treatment('employee_user_01','view_main_list', {age: 35})
|
|
131
148
|
end
|
132
149
|
```
|
133
150
|
|
151
|
+
Also, you can use different keys for actually getting treatment and sending impressions to the server:
|
152
|
+
```ruby
|
153
|
+
split_client.get_treatment({ matching_key: 'user_id', bucketing_key: 'private_user_id' },'feature_name', {attr: 'val'})
|
154
|
+
```
|
155
|
+
When it might be useful? Say, you have a user browsing your website and not signed up yet. You assign some internal id to that user (i.e. bucketing_key) and after user signs up you assign him a matching_key.
|
156
|
+
By doing this you can provide both anonymous and signed up user with the same treatment.
|
157
|
+
|
158
|
+
`bucketing_key` may be `nil` in that case `matching_key` would be used as a key, so calling
|
159
|
+
```ruby
|
160
|
+
split_client.get_treatment({ matching_key: 'user_id' },'feature_name', {attr: 'val'})
|
161
|
+
```
|
162
|
+
Is exactly the same as calling
|
163
|
+
```ruby
|
164
|
+
split_client.get_treatment('user_id' ,'feature_name', {attr: 'val'})
|
165
|
+
```
|
166
|
+
`bucketing_key` must not be nil
|
167
|
+
|
134
168
|
Also you can use the split manager:
|
135
169
|
|
136
170
|
```ruby
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SplitIoClient
|
2
|
+
module Cache
|
3
|
+
module Adapters
|
4
|
+
class Adapter
|
5
|
+
def set
|
6
|
+
raise NoMethodError
|
7
|
+
end
|
8
|
+
|
9
|
+
def get
|
10
|
+
raise NoMethodError
|
11
|
+
end
|
12
|
+
|
13
|
+
def remove
|
14
|
+
raise NoMethodError
|
15
|
+
end
|
16
|
+
|
17
|
+
def key?
|
18
|
+
raise NoMethodError
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
|
3
|
+
module SplitIoClient
|
4
|
+
module Cache
|
5
|
+
module Adapters
|
6
|
+
class MemoryAdapter < Adapter
|
7
|
+
def initialize
|
8
|
+
@map = Concurrent::Map.new
|
9
|
+
end
|
10
|
+
|
11
|
+
# Map
|
12
|
+
def [](key)
|
13
|
+
@map[key]
|
14
|
+
end
|
15
|
+
|
16
|
+
def []=(key, obj)
|
17
|
+
@map[key] = obj
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize_map(key)
|
21
|
+
@map[key] = Concurrent::Map.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_to_map(key, map_key, map_value)
|
25
|
+
@map[key].put(map_key, map_value)
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_in_map(key, map_key)
|
29
|
+
return nil if @map[key].nil?
|
30
|
+
|
31
|
+
@map[key].get(map_key)
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete_from_map(key, map_key)
|
35
|
+
@map[key].delete(map_key)
|
36
|
+
end
|
37
|
+
|
38
|
+
def in_map?(key, map_key)
|
39
|
+
return false if @map[key].nil?
|
40
|
+
|
41
|
+
@map[key].key?(map_key)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SplitIoClient
|
2
|
+
module Cache
|
3
|
+
class Repository
|
4
|
+
def initialize(adapter)
|
5
|
+
@adapter = adapter
|
6
|
+
|
7
|
+
@adapter[namespace_key('ready')] = false
|
8
|
+
end
|
9
|
+
|
10
|
+
def []=(key, obj)
|
11
|
+
@adapter[namespace_key(key)] = obj
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
@adapter[namespace_key(key)]
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def namespace_key(key)
|
21
|
+
"repository_#{key}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module SplitIoClient
|
2
|
+
module Cache
|
3
|
+
module Repositories
|
4
|
+
class SegmentsRepository < Repository
|
5
|
+
def add_to_segment(segment)
|
6
|
+
name = segment[:name]
|
7
|
+
|
8
|
+
@adapter.initialize_map(namespace_key("segments:#{name}")) if @adapter[namespace_key("segments:#{name}")].nil?
|
9
|
+
|
10
|
+
add_keys(name, segment[:added])
|
11
|
+
remove_keys(name, segment[:removed])
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_segment_keys(name)
|
15
|
+
@adapter[namespace_key("segments:#{name}")]
|
16
|
+
end
|
17
|
+
|
18
|
+
def in_segment?(name, key)
|
19
|
+
@adapter.in_map?(namespace_key("segments:#{name}"), key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def used_segment_names
|
23
|
+
@adapter['splits_repository_used_segment_names'].keys
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_change_number(name, last_change)
|
27
|
+
@adapter.initialize_map(namespace_key('changes')) if @adapter[namespace_key('changes')].nil?
|
28
|
+
|
29
|
+
@adapter.add_to_map(namespace_key('changes'), name, last_change)
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_change_number(name)
|
33
|
+
@adapter.find_in_map(namespace_key('changes'), name) || -1
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def namespace_key(key)
|
39
|
+
"segments_repository_#{key}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_keys(name, keys)
|
43
|
+
keys.each { |key| @adapter.add_to_map(namespace_key("segments:#{name}"), key, 1) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def remove_keys(name, keys)
|
47
|
+
keys.each { |key| @adapter.delete_from_map(namespace_key("segments:#{name}"), key) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module SplitIoClient
|
2
|
+
module Cache
|
3
|
+
module Repositories
|
4
|
+
class SplitsRepository < Repository
|
5
|
+
def initialize(adapter)
|
6
|
+
@adapter = adapter
|
7
|
+
|
8
|
+
@adapter[namespace_key('last_change')] = -1
|
9
|
+
@adapter.initialize_map(namespace_key('splits'))
|
10
|
+
@adapter.initialize_map(namespace_key('used_segment_names'))
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_split(split)
|
14
|
+
split_without_name = split.select { |k, _| k != :name }
|
15
|
+
|
16
|
+
@adapter.add_to_map(namespace_key('splits'), split[:name], split_without_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def remove_split(name)
|
20
|
+
@adapter.add_to_map(namespace_key('splits'), name, nil)
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_split(name)
|
24
|
+
@adapter.find_in_map(namespace_key('splits'), name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_change_number(since)
|
28
|
+
@adapter[namespace_key('last_change')] = since
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_change_number
|
32
|
+
@adapter[namespace_key('last_change')]
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_segment_names(names)
|
36
|
+
return if names.nil? || names.empty?
|
37
|
+
|
38
|
+
names.each do |name|
|
39
|
+
@adapter.add_to_map(namespace_key('used_segment_names'), name, 1)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def namespace_key(key)
|
46
|
+
"splits_repository_#{key}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module SplitIoClient
|
5
|
+
module Cache
|
6
|
+
module Stores
|
7
|
+
class SDKBlocker
|
8
|
+
attr_reader :splits_ready
|
9
|
+
attr_writer :splits_thread, :segments_thread
|
10
|
+
|
11
|
+
def initialize(config)
|
12
|
+
@config = config
|
13
|
+
|
14
|
+
@splits_ready = false
|
15
|
+
@segments_ready = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def splits_ready!
|
19
|
+
@splits_ready = true
|
20
|
+
end
|
21
|
+
|
22
|
+
def segments_ready!
|
23
|
+
@segments_ready = true
|
24
|
+
end
|
25
|
+
|
26
|
+
def block
|
27
|
+
begin
|
28
|
+
Timeout::timeout(@config.block_until_ready) do
|
29
|
+
sleep 0.1 until ready?
|
30
|
+
sleep 0.1 until ready?
|
31
|
+
end
|
32
|
+
rescue Timeout::Error
|
33
|
+
fail SDKBlockerTimeoutExpiredException, 'SDK start up timeout expired'
|
34
|
+
end
|
35
|
+
|
36
|
+
@config.logger.info('SplitIO SDK is ready')
|
37
|
+
@splits_thread.run
|
38
|
+
@segments_thread.run
|
39
|
+
end
|
40
|
+
|
41
|
+
def ready?
|
42
|
+
@splits_ready && @segments_ready
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module SplitIoClient
|
2
|
+
module Cache
|
3
|
+
module Stores
|
4
|
+
class SegmentStore
|
5
|
+
attr_reader :segments_repository
|
6
|
+
|
7
|
+
def initialize(segments_repository, config, api_key, metrics, sdk_blocker = nil)
|
8
|
+
@segments_repository = segments_repository
|
9
|
+
@config = config
|
10
|
+
@api_key = api_key
|
11
|
+
@metrics = metrics
|
12
|
+
@sdk_blocker = sdk_blocker
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
if ENV['SPLITCLIENT_ENV'] == 'test'
|
17
|
+
store_segments
|
18
|
+
else
|
19
|
+
@sdk_blocker.segments_thread = Thread.new do
|
20
|
+
@config.block_until_ready ? blocked_store : unblocked_store
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def blocked_store
|
28
|
+
loop do
|
29
|
+
next unless @sdk_blocker.splits_ready
|
30
|
+
|
31
|
+
store_segments
|
32
|
+
@config.logger.debug("Segment names: #{@segments_repository.used_segment_names.to_a}") if @config.debug_enabled
|
33
|
+
|
34
|
+
unless @sdk_blocker.ready?
|
35
|
+
@sdk_blocker.segments_ready!
|
36
|
+
@config.logger.info('segments are ready')
|
37
|
+
end
|
38
|
+
|
39
|
+
sleep_for = random_interval(@config.segments_refresh_rate)
|
40
|
+
@config.logger.debug("Segments store is sleeping for: #{sleep_for} seconds") if @config.debug_enabled
|
41
|
+
sleep(sleep_for)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def unblocked_store
|
46
|
+
loop do
|
47
|
+
store_segments
|
48
|
+
|
49
|
+
sleep(random_interval(@config.segments_refresh_rate))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def store_segments
|
54
|
+
segments_api.store_segments_by_names(@segments_repository.used_segment_names)
|
55
|
+
rescue StandardError => error
|
56
|
+
@config.log_found_exception(__method__.to_s, error)
|
57
|
+
end
|
58
|
+
|
59
|
+
def random_interval(interval)
|
60
|
+
random_factor = Random.new.rand(50..100) / 100.0
|
61
|
+
|
62
|
+
interval * random_factor
|
63
|
+
end
|
64
|
+
|
65
|
+
def segments_api
|
66
|
+
SplitIoClient::Api::Segments.new(@api_key, @config, @metrics, @segments_repository)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module SplitIoClient
|
2
|
+
module Cache
|
3
|
+
module Stores
|
4
|
+
class SplitStore
|
5
|
+
attr_reader :splits_repository
|
6
|
+
|
7
|
+
def initialize(splits_repository, config, api_key, metrics, sdk_blocker = nil)
|
8
|
+
@splits_repository = splits_repository
|
9
|
+
@config = config
|
10
|
+
@api_key = api_key
|
11
|
+
@metrics = metrics
|
12
|
+
@sdk_blocker = sdk_blocker
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
if ENV['SPLITCLIENT_ENV'] == 'test'
|
17
|
+
store_splits
|
18
|
+
else
|
19
|
+
@sdk_blocker.splits_thread = Thread.new do
|
20
|
+
loop do
|
21
|
+
store_splits
|
22
|
+
|
23
|
+
sleep(random_interval(@config.features_refresh_rate))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def store_splits
|
32
|
+
data = splits_since(@splits_repository.get_change_number)
|
33
|
+
|
34
|
+
data[:splits] && data[:splits].each do |split|
|
35
|
+
@splits_repository.add_split(split)
|
36
|
+
end
|
37
|
+
|
38
|
+
@splits_repository.set_segment_names(data[:segment_names])
|
39
|
+
@splits_repository.set_change_number(data[:till])
|
40
|
+
|
41
|
+
@config.logger.debug("segments seen(#{data[:segment_names].length()}): #{data[:segment_names].to_a}") if @config.debug_enabled
|
42
|
+
|
43
|
+
if @config.block_until_ready && !@sdk_blocker.ready?
|
44
|
+
@sdk_blocker.splits_ready!
|
45
|
+
@config.logger.info('splits are ready')
|
46
|
+
end
|
47
|
+
|
48
|
+
rescue StandardError => error
|
49
|
+
@config.log_found_exception(__method__.to_s, error)
|
50
|
+
end
|
51
|
+
|
52
|
+
def random_interval(interval)
|
53
|
+
random_factor = Random.new.rand(50..100) / 100.0
|
54
|
+
|
55
|
+
interval * random_factor
|
56
|
+
end
|
57
|
+
|
58
|
+
def splits_since(since)
|
59
|
+
SplitIoClient::Api::Splits.new(@api_key, @config, @metrics).since(since)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|