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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.txt +12 -0
  3. data/NEWS +4 -0
  4. data/README.md +45 -11
  5. data/lib/cache/adapters/adapter.rb +23 -0
  6. data/lib/cache/adapters/memory_adapter.rb +46 -0
  7. data/lib/cache/repositories/repository.rb +25 -0
  8. data/lib/cache/repositories/segments_repository.rb +52 -0
  9. data/lib/cache/repositories/splits_repository.rb +51 -0
  10. data/lib/cache/stores/sdk_blocker.rb +47 -0
  11. data/lib/cache/stores/segment_store.rb +71 -0
  12. data/lib/cache/stores/split_store.rb +64 -0
  13. data/lib/engine/api/client.rb +29 -0
  14. data/lib/engine/api/segments.rb +60 -0
  15. data/lib/engine/api/splits.rb +58 -0
  16. data/lib/{splitclient-engine → engine}/evaluator/splitter.rb +0 -0
  17. data/lib/{splitclient-engine → engine}/impressions/impressions.rb +0 -0
  18. data/lib/{splitclient-engine → engine}/matchers/all_keys_matcher.rb +0 -0
  19. data/lib/{splitclient-engine → engine}/matchers/between_matcher.rb +2 -0
  20. data/lib/{splitclient-engine → engine}/matchers/combiners.rb +0 -0
  21. data/lib/{splitclient-engine → engine}/matchers/combining_matcher.rb +1 -1
  22. data/lib/{splitclient-engine → engine}/matchers/equal_to_matcher.rb +0 -0
  23. data/lib/{splitclient-engine → engine}/matchers/greater_than_or_equal_to_matcher.rb +0 -0
  24. data/lib/{splitclient-engine → engine}/matchers/less_than_or_equal_to_matcher.rb +0 -0
  25. data/lib/{splitclient-engine → engine}/matchers/negation_matcher.rb +0 -0
  26. data/lib/{splitclient-engine → engine}/matchers/user_defined_segment_matcher.rb +4 -21
  27. data/lib/{splitclient-engine → engine}/matchers/whitelist_matcher.rb +0 -0
  28. data/lib/{splitclient-engine → engine}/metrics/binary_search_latency_tracker.rb +0 -0
  29. data/lib/{splitclient-engine → engine}/metrics/metrics.rb +0 -0
  30. data/lib/{splitclient-engine → engine}/parser/condition.rb +5 -7
  31. data/lib/{splitclient-engine → engine}/parser/partition.rb +0 -0
  32. data/lib/{splitclient-engine → engine}/parser/split.rb +11 -3
  33. data/lib/{splitclient-engine → engine}/parser/split_adapter.rb +20 -184
  34. data/lib/engine/parser/split_treatment.rb +65 -0
  35. data/lib/{splitclient-engine → engine}/partitions/treatments.rb +0 -0
  36. data/lib/exceptions/sdk_blocker_timeout_expired_exception.rb +4 -0
  37. data/lib/splitclient-rb.rb +31 -23
  38. data/lib/splitclient-rb/split_config.rb +41 -4
  39. data/lib/splitclient-rb/split_factory.rb +50 -20
  40. data/lib/splitclient-rb/version.rb +1 -1
  41. data/splitclient-rb.gemspec +2 -0
  42. metadata +62 -25
  43. data/lib/splitclient-cache/local_store.rb +0 -45
  44. data/lib/splitclient-engine/parser/segment.rb +0 -84
  45. data/lib/splitclient-engine/parser/segment_parser.rb +0 -46
  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: 15187969c2e35ddd3cfaf21e25f0cdbbf8daa826
4
- data.tar.gz: 543a3ca07e3724d5dace9b920bddb9adbdc33046
3
+ metadata.gz: ae8492b8f79d39cda6b4c52bb8300c8d7374fef4
4
+ data.tar.gz: '09129f27b81bd42cea7731786f62b4bffdbc5c96'
5
5
  SHA512:
6
- metadata.gz: 20c99dc8e41344b8afc83bddf87cd20d653b34936aeed35c4a9717e3d2656eaa4a6b7b787795d95b6291bd61dc9754f5ae0ec6525d06490866f9cf735c597ee3
7
- data.tar.gz: f2a7947a3de5d026232156061916f49154293dd90d425486f02c9449f16d48edb9df09f244603a471f669ff7811f6a10c1c34515cfd053faa19554886b6fe521
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
@@ -1,3 +1,7 @@
1
+ 3.0.2
2
+
3
+ now support also client.get_treatment( { :matching_key = 'bb' , "bucketing_key = ''}, ....)
4
+
1
5
  2.0.1
2
6
 
3
7
  No news for this release
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 = {base_uri: 'https://my.app.api/',
109
- local_store: Rails.cache,
110
- connection_timeout: 10,
111
- read_timeout: 5,
112
- features_refresh_rate: 120,
113
- segments_refresh_rate: 120,
114
- metrics_refresh_rate: 360,
115
- impressions_refresh_rate: 360,
116
- logger: Logger.new('logfile.log')}
117
-
118
- split_client = SplitIoClient::SplitFactory.new("your_api_key", options).client
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