kameleoon-client-ruby 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/kameleoon/client.rb +224 -153
- data/lib/kameleoon/configuration/feature_flag.rb +13 -24
- data/lib/kameleoon/configuration/rule.rb +17 -5
- data/lib/kameleoon/configuration/settings.rb +20 -0
- data/lib/kameleoon/cookie.rb +3 -3
- data/lib/kameleoon/data.rb +24 -18
- data/lib/kameleoon/hybrid/manager.rb +60 -0
- data/lib/kameleoon/real_time/real_time_configuration_service.rb +98 -0
- data/lib/kameleoon/real_time/real_time_event.rb +22 -0
- data/lib/kameleoon/real_time/sse_client.rb +111 -0
- data/lib/kameleoon/real_time/sse_message.rb +23 -0
- data/lib/kameleoon/real_time/sse_request.rb +59 -0
- data/lib/kameleoon/storage/cache.rb +84 -0
- data/lib/kameleoon/storage/cache_factory.rb +23 -0
- data/lib/kameleoon/targeting/condition.rb +4 -4
- data/lib/kameleoon/targeting/conditions/custom_datum.rb +60 -25
- data/lib/kameleoon/targeting/conditions/exclusive_experiment.rb +1 -1
- data/lib/kameleoon/targeting/conditions/target_experiment.rb +2 -2
- data/lib/kameleoon/version.rb +1 -1
- metadata +25 -3
- data/lib/kameleoon/configuration/feature_flag_v2.rb +0 -30
@@ -14,25 +14,29 @@ module Kameleoon
|
|
14
14
|
|
15
15
|
# Rule is a class for new rules of feature flags
|
16
16
|
class Rule
|
17
|
-
|
17
|
+
attr_reader :id, :order, :type, :exposition, :experiment_id, :variation_by_exposition, :respool_time
|
18
|
+
attr_accessor :targeting_segment
|
18
19
|
|
19
20
|
def self.create_from_array(array)
|
20
21
|
array&.map { |it| Rule.new(it) }
|
21
22
|
end
|
22
23
|
|
23
24
|
def initialize(hash)
|
25
|
+
@id = hash['id']
|
26
|
+
@order = hash['order']
|
24
27
|
@type = hash['type']
|
25
28
|
@exposition = hash['exposition']
|
26
29
|
@experiment_id = hash['experimentId']
|
30
|
+
@respool_time = hash['respoolTime']
|
27
31
|
@variation_by_exposition = VariationByExposition.create_from_array(hash['variationByExposition'])
|
28
32
|
@targeting_segment = Kameleoon::Targeting::Segment.new((hash['segment'])) if hash['segment']
|
29
33
|
end
|
30
34
|
|
31
|
-
def
|
35
|
+
def get_variation(hash_double)
|
32
36
|
total = 0.0
|
33
|
-
variation_by_exposition.each do |
|
34
|
-
total +=
|
35
|
-
return
|
37
|
+
variation_by_exposition.each do |var_by_exp|
|
38
|
+
total += var_by_exp.exposition
|
39
|
+
return var_by_exp if total >= hash_double
|
36
40
|
end
|
37
41
|
nil
|
38
42
|
end
|
@@ -40,6 +44,14 @@ module Kameleoon
|
|
40
44
|
def get_variation_id_by_key(key)
|
41
45
|
variation_by_exposition.select { |v| v.variation_key == key }.first&.variation_id
|
42
46
|
end
|
47
|
+
|
48
|
+
def experiment_type?
|
49
|
+
@type == RuleType::EXPERIMENTATION
|
50
|
+
end
|
51
|
+
|
52
|
+
def targeted_delivery_type?
|
53
|
+
@type == RuleType::TARGETED_DELIVERY
|
54
|
+
end
|
43
55
|
end
|
44
56
|
end
|
45
57
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kameleoon
|
4
|
+
# Module which contains all internal data of SDK
|
5
|
+
module Configuration
|
6
|
+
# KameleoonConfigurationSettings is used for saving setting's parameters, e.g
|
7
|
+
# state of real time update for site code and etc
|
8
|
+
class Settings
|
9
|
+
attr_accessor :real_time_update
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@real_time_update = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def update(configuration)
|
16
|
+
@real_time_update = configuration['realTimeUpdate'] || false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/kameleoon/cookie.rb
CHANGED
@@ -26,7 +26,7 @@ module Kameleoon
|
|
26
26
|
obtain_hash_double_helper(visitor_code, respool_times, container_id, '')
|
27
27
|
end
|
28
28
|
|
29
|
-
def
|
29
|
+
def obtain_hash_double_rule(visitor_code, container_id = '', suffix = '')
|
30
30
|
obtain_hash_double_helper(visitor_code, {}, container_id, suffix)
|
31
31
|
end
|
32
32
|
|
@@ -37,8 +37,8 @@ module Kameleoon
|
|
37
37
|
identifier += respool_times.sort.to_h.values.join.to_s if !respool_times.nil? && !respool_times.empty?
|
38
38
|
(Digest::SHA256.hexdigest(identifier.encode('UTF-8')).to_i(16) / (BigDecimal('2')**BigDecimal('256'))).round(16)
|
39
39
|
end
|
40
|
-
|
41
|
-
def check_visitor_code(visitor_code)
|
40
|
+
|
41
|
+
def check_visitor_code(visitor_code)
|
42
42
|
if visitor_code.nil?
|
43
43
|
check_default_visitor_code('')
|
44
44
|
elsif
|
data/lib/kameleoon/data.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'kameleoon/exceptions'
|
4
|
+
|
3
5
|
module Kameleoon
|
4
6
|
NONCE_LENGTH = 16
|
5
7
|
|
@@ -50,38 +52,42 @@ module Kameleoon
|
|
50
52
|
end
|
51
53
|
|
52
54
|
class CustomData < Data
|
53
|
-
|
55
|
+
attr_reader :id, :values
|
54
56
|
|
55
57
|
# @param [Integer] id Id of the custom data
|
56
58
|
# @param [String] value Value of the custom data
|
57
59
|
#
|
58
60
|
# @overload
|
59
61
|
# @param [Hash] hash Json value encoded in a hash.
|
60
|
-
def initialize(*args)
|
62
|
+
def initialize(arg0, *args)
|
61
63
|
@instance = DataType::CUSTOM
|
62
64
|
@sent = false
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
@
|
76
|
-
@value = args[1]
|
65
|
+
if arg0.is_a?(Hash)
|
66
|
+
hash = arg0
|
67
|
+
id = hash['id']
|
68
|
+
raise Kameleoon::Exception::NotFound.new('id') if id.nil?
|
69
|
+
@id = id.to_s
|
70
|
+
value = hash['value']
|
71
|
+
values = hash['values']
|
72
|
+
raise Kameleoon::Exception::NotFound.new('values') if values.nil? && value.nil?
|
73
|
+
if values.nil?
|
74
|
+
@values = [value]
|
75
|
+
else
|
76
|
+
@values = values.dup
|
77
|
+
@values.append(value) unless value.nil?
|
77
78
|
end
|
79
|
+
else
|
80
|
+
@id = arg0.to_s
|
81
|
+
@values = args
|
78
82
|
end
|
79
83
|
end
|
80
84
|
|
81
85
|
def obtain_full_post_text_line
|
82
|
-
|
86
|
+
return '' if @values.empty?
|
87
|
+
|
88
|
+
str_values = "[[\"#{@values.join('",1],["')}\",1]]"
|
83
89
|
nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
|
84
|
-
"eventType=customData&index
|
90
|
+
"eventType=customData&index=#{@id}&valueToCount=#{encode(str_values)}&overwrite=true&nonce=#{nonce}"
|
85
91
|
end
|
86
92
|
end
|
87
93
|
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kameleoon
|
4
|
+
module Hybrid
|
5
|
+
TC_INIT = 'window.kameleoonQueue=window.kameleoonQueue||[];'
|
6
|
+
TC_ASSIGN_VARIATION_FORMAT = "window.kameleoonQueue.push(['Experiments.assignVariation',%d,%d]);"
|
7
|
+
TC_TRIGGER_FORMAT = "window.kameleoonQueue.push(['Experiments.trigger',%d,true]);"
|
8
|
+
TC_ASSIGN_VARIATION_TRIGGER_FORMAT = TC_ASSIGN_VARIATION_FORMAT + TC_TRIGGER_FORMAT
|
9
|
+
|
10
|
+
# Will be useful for Ruby 3.0
|
11
|
+
# Abstract Manager class (interface)
|
12
|
+
class Manager
|
13
|
+
def add_variation(_visitor, _experiment_id, _variation_id)
|
14
|
+
raise 'Abstract method `add` called'
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_engine_tracking_code(_visitor)
|
18
|
+
raise 'Abstract method `get_engine_tracking_code` called'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Implementation of Cache with auto cleaning feature
|
23
|
+
class ManagerImpl < Manager
|
24
|
+
def initialize(expiration_time, cleaning_interval, cache_factory, log_func)
|
25
|
+
super()
|
26
|
+
# synchronization is necessary for adding same visitor_code from different threads
|
27
|
+
@mutex = Mutex.new
|
28
|
+
@expiration_time = expiration_time
|
29
|
+
@cache_factory = cache_factory
|
30
|
+
@log = log_func
|
31
|
+
# it's recommend to use cleaning_interval 3-4 times more than experiation_time for more performance
|
32
|
+
# in this case on every cleaning iteration storage will be cleaned 2/3 - 3/4 of volume
|
33
|
+
@cache = cache_factory.create(expiration_time, cleaning_interval)
|
34
|
+
@log.call('Hybrid Manager was successfully initialized')
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_variation(visitor_code, experiment_id, variation_id)
|
38
|
+
@mutex.synchronize do
|
39
|
+
visitor_cache = @cache.get(visitor_code)
|
40
|
+
visitor_cache = @cache_factory.create(@expiration_time, 0) if visitor_cache.nil?
|
41
|
+
visitor_cache.set(experiment_id, variation_id)
|
42
|
+
@cache.set(visitor_code, visitor_cache)
|
43
|
+
end
|
44
|
+
@log.call("Hybrid Manager successfully added variation for visitor_code: #{visitor_code}, " \
|
45
|
+
"experiment: #{experiment_id}, variation: #{variation_id}")
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_engine_tracking_code(visitor_code)
|
49
|
+
tracking_code = TC_INIT
|
50
|
+
visitor_cache = @cache.get(visitor_code)
|
51
|
+
return tracking_code if visitor_cache.nil?
|
52
|
+
|
53
|
+
visitor_cache.active_items.each_pair do |key, value|
|
54
|
+
tracking_code += format(TC_ASSIGN_VARIATION_TRIGGER_FORMAT, key, value, key)
|
55
|
+
end
|
56
|
+
tracking_code
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Kameleoon Real Time Configuration Service
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
require 'kameleoon/real_time/real_time_event'
|
7
|
+
require 'kameleoon/real_time/sse_request'
|
8
|
+
require 'kameleoon/real_time/sse_client'
|
9
|
+
|
10
|
+
module Kameleoon
|
11
|
+
module RealTime
|
12
|
+
CONFIGURATION_UPDATE_EVENT = 'configuration-update-event'
|
13
|
+
|
14
|
+
##
|
15
|
+
# RealTimeConfigurationService is used for fetching updates of configuration
|
16
|
+
# (experiments and feature flags) in real time.
|
17
|
+
class RealTimeConfigurationService
|
18
|
+
##
|
19
|
+
# Parametrized initializer.
|
20
|
+
#
|
21
|
+
# @param url [String]
|
22
|
+
# @param update_handler [Callable[Kameleoon::RealTime::RealTimeEvent] | NilClass] Handler which
|
23
|
+
# is synchronously called for gotten RealTimeEvent objects.
|
24
|
+
# @param log_func [Callable[String] | NilClass] Callable object which synchronously called to log.
|
25
|
+
def initialize(url, update_handler, log_func, sse_request_source = nil)
|
26
|
+
@url = url
|
27
|
+
@update_handler = update_handler
|
28
|
+
@need_close = false
|
29
|
+
@headers = {
|
30
|
+
'Accept': 'text/event-stream',
|
31
|
+
'Cache-Control': 'no-cache',
|
32
|
+
'Connection': 'Keep-Alive'
|
33
|
+
}
|
34
|
+
@log_func = log_func
|
35
|
+
@sse_request_source = sse_request_source
|
36
|
+
@sse_thread = nil
|
37
|
+
@sse_client = nil
|
38
|
+
create_sse_client
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Closes the connection to the server.
|
43
|
+
def close
|
44
|
+
return if @need_close
|
45
|
+
|
46
|
+
@log_func&.call('Real-time configuration service is shutting down')
|
47
|
+
@need_close = true
|
48
|
+
return if @sse_thread.nil?
|
49
|
+
|
50
|
+
@sse_thread.kill
|
51
|
+
@sse_thread = nil
|
52
|
+
@sse_client.call_close_handler
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def create_sse_client
|
58
|
+
@sse_thread = Thread.new { run_sse_client } unless @need_close
|
59
|
+
end
|
60
|
+
|
61
|
+
def init_sse_client
|
62
|
+
message_handler = proc do |message|
|
63
|
+
@log_func&.call("Got SSE event: #{message.event}")
|
64
|
+
if message.event == CONFIGURATION_UPDATE_EVENT
|
65
|
+
event_dict = JSON.parse(message.data)
|
66
|
+
@update_handler&.call(RealTimeEvent.new(event_dict))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
sse_request = make_sse_request
|
70
|
+
@sse_client = SseClient.new(sse_request, message_handler)
|
71
|
+
@log_func&.call('Created SSE client')
|
72
|
+
end
|
73
|
+
|
74
|
+
def make_sse_request
|
75
|
+
open_handler = proc { @log_func&.call('SSE connection open') }
|
76
|
+
close_handler = proc { @log_func&.call('SSE connection closed') }
|
77
|
+
unexpected_status_code_handler =
|
78
|
+
proc { |resp| @log_func&.call("Unexpected status code of SSE response: #{resp.code}") }
|
79
|
+
if @sse_request_source.nil?
|
80
|
+
return SseRequest.new(@url, @headers, open_handler, close_handler, unexpected_status_code_handler)
|
81
|
+
end
|
82
|
+
|
83
|
+
@sse_request_source.call(@url, @headers, open_handler, close_handler, unexpected_status_code_handler)
|
84
|
+
end
|
85
|
+
|
86
|
+
def run_sse_client
|
87
|
+
until @need_close
|
88
|
+
init_sse_client
|
89
|
+
begin
|
90
|
+
@sse_client.start
|
91
|
+
rescue StandardError => e
|
92
|
+
@log_func&.call("Error occurred within SSE client: #{e}")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Kameleoon Real Time Event
|
4
|
+
|
5
|
+
module Kameleoon
|
6
|
+
module RealTime
|
7
|
+
##
|
8
|
+
# RealTimeEvent contains information about timestamp when configuration was updated.
|
9
|
+
# Timestamp parameter is used to fetch the latest configuration.
|
10
|
+
class RealTimeEvent
|
11
|
+
attr_reader :time_stamp
|
12
|
+
|
13
|
+
##
|
14
|
+
# Parametrized initializer. Gets the new time stamp from a JSON message.
|
15
|
+
#
|
16
|
+
# @param event_dict [Hash] Hash of SSE message data.
|
17
|
+
def initialize(event_dict)
|
18
|
+
@time_stamp = event_dict['ts']
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kameleoon/real_time/sse_message'
|
4
|
+
|
5
|
+
module Kameleoon
|
6
|
+
module RealTime
|
7
|
+
##
|
8
|
+
# SseClient is used to interpret SSE event stream.
|
9
|
+
class SseClient
|
10
|
+
##
|
11
|
+
# Parametrized initializer.
|
12
|
+
#
|
13
|
+
# @param sse_request [Kameleoon::RealTime::SseRequest] Used to access SSE event stream.
|
14
|
+
# @param message_handler [Callable[Kameleoon::RealTime::SseMessage] | NilClass] Callable object which
|
15
|
+
# is synchronously called for received SSE messages.
|
16
|
+
def initialize(sse_request, message_handler)
|
17
|
+
@sse_request = sse_request
|
18
|
+
@message_handler = message_handler
|
19
|
+
|
20
|
+
@cr_prev = nil
|
21
|
+
@buffer = nil
|
22
|
+
@data_buffer = nil
|
23
|
+
@event = nil
|
24
|
+
@id = nil
|
25
|
+
@reconnection_time = nil
|
26
|
+
|
27
|
+
@sse_request.resp_char_handler = method(:process_char)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Starts SSE connection and stay in the loop until close.
|
32
|
+
def start
|
33
|
+
@cr_prev = false
|
34
|
+
@data_buffer = []
|
35
|
+
@event = nil
|
36
|
+
@id = nil
|
37
|
+
@buffer = []
|
38
|
+
@sse_request.start
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Calls @sse_request@close_handler if it is not nil.
|
43
|
+
def call_close_handler
|
44
|
+
@sse_request.call_close_handler
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def process_char(ch)
|
50
|
+
if @cr_prev && (ch == "\n")
|
51
|
+
@cr_prev = false
|
52
|
+
return
|
53
|
+
end
|
54
|
+
@cr_prev = ch == "\r"
|
55
|
+
if @cr_prev || (ch == "\n")
|
56
|
+
line = @buffer.join
|
57
|
+
@buffer.clear
|
58
|
+
if line.empty?
|
59
|
+
dispatch_event
|
60
|
+
else
|
61
|
+
handle_line(line)
|
62
|
+
end
|
63
|
+
return
|
64
|
+
end
|
65
|
+
@buffer << ch
|
66
|
+
end
|
67
|
+
|
68
|
+
def handle_line(line)
|
69
|
+
field, value = parse_line(line)
|
70
|
+
return if field.nil?
|
71
|
+
|
72
|
+
case field
|
73
|
+
when 'event'
|
74
|
+
@event = value
|
75
|
+
when 'data'
|
76
|
+
@data_buffer << value
|
77
|
+
when 'id'
|
78
|
+
@id = value
|
79
|
+
when 'retry'
|
80
|
+
@reconnection_time = value.to_i # This does not affect anything
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def parse_line(line)
|
85
|
+
colon_index = line.index(':')
|
86
|
+
return line, nil if colon_index.nil?
|
87
|
+
return nil, nil if colon_index.zero?
|
88
|
+
|
89
|
+
field = line[0, colon_index]
|
90
|
+
return field, '' if colon_index + 1 == line.length
|
91
|
+
|
92
|
+
value = if line[colon_index + 1] == ' '
|
93
|
+
line[colon_index + 2..]
|
94
|
+
else
|
95
|
+
line[colon_index + 1..]
|
96
|
+
end
|
97
|
+
[field, value]
|
98
|
+
end
|
99
|
+
|
100
|
+
def dispatch_event
|
101
|
+
return unless !@event.nil? || !@data_buffer.empty? # Ignoring empty events
|
102
|
+
|
103
|
+
data = @data_buffer.join("\n")
|
104
|
+
message = SseMessage.new(@event, @id, data)
|
105
|
+
@event = nil
|
106
|
+
@data_buffer.clear
|
107
|
+
@message_handler&.call(message)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kameleoon
|
4
|
+
module RealTime
|
5
|
+
##
|
6
|
+
# SseMessage is a container for a received SSE message.
|
7
|
+
class SseMessage
|
8
|
+
attr_reader :event, :id, :data
|
9
|
+
|
10
|
+
##
|
11
|
+
# Parametrized initializer.
|
12
|
+
#
|
13
|
+
# @param event [String]
|
14
|
+
# @param id [String]
|
15
|
+
# @param data [String]
|
16
|
+
def initialize(event, id, data)
|
17
|
+
@event = event
|
18
|
+
@id = id
|
19
|
+
@data = data
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'restclient'
|
4
|
+
|
5
|
+
module Kameleoon
|
6
|
+
module RealTime
|
7
|
+
##
|
8
|
+
# SseRequest is used keep SSE connection and read from its stream.
|
9
|
+
class SseRequest
|
10
|
+
attr_writer :resp_char_handler
|
11
|
+
|
12
|
+
##
|
13
|
+
# Parametrized initializer.
|
14
|
+
#
|
15
|
+
# @param url [String]
|
16
|
+
# @param headers [Hash]
|
17
|
+
# @param open_handler [Callable | NilClass] Handler which is synchronously called when SSE connection is open.
|
18
|
+
# @param close_handler [Callable | NilClass] Handler which is synchronously called when SSE connection is closed.
|
19
|
+
# @param unexpected_status_code_handler [Callable[Net::HTTPResponse] | NilClass] Handler whick
|
20
|
+
# is synchronously called for responses with not 200 status code.
|
21
|
+
def initialize(url, headers, open_handler, close_handler, unexpected_status_code_handler)
|
22
|
+
@url = url
|
23
|
+
@headers = headers
|
24
|
+
@open_handler = open_handler
|
25
|
+
@close_handler = close_handler
|
26
|
+
@unexpected_status_code_handler = unexpected_status_code_handler
|
27
|
+
|
28
|
+
@resp_char_handler = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Starts SSE connection and stay in the loop until close.
|
33
|
+
def start
|
34
|
+
RestClient::Request.execute(method: :get, url: @url, headers: @headers, read_timeout: nil,
|
35
|
+
block_response: method(:handle_resp))
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Calls @close_handler if it is not nil.
|
40
|
+
def call_close_handler
|
41
|
+
@close_handler&.call
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def handle_resp(resp)
|
47
|
+
@open_handler&.call
|
48
|
+
if resp.code == '200'
|
49
|
+
resp.read_body do |chunk|
|
50
|
+
chunk.each_char { |ch| @resp_char_handler&.call(ch) }
|
51
|
+
end
|
52
|
+
else
|
53
|
+
@unexpected_status_code_handler&.call(resp)
|
54
|
+
end
|
55
|
+
call_close_handler
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent'
|
4
|
+
|
5
|
+
module Kameleoon
|
6
|
+
module Storage
|
7
|
+
# Will be useful for Ruby 3.0
|
8
|
+
# Abstract Cache class (interface)
|
9
|
+
class Cache
|
10
|
+
def get(_key)
|
11
|
+
raise 'Abstract method `read` called'
|
12
|
+
end
|
13
|
+
|
14
|
+
def set(_key, _value)
|
15
|
+
raise 'Abstract method `write` called'
|
16
|
+
end
|
17
|
+
|
18
|
+
def active_items
|
19
|
+
raise 'Abstract method `active_items` called'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Implementation of Cache with auto cleaning feature
|
24
|
+
class CacheImpl < Cache
|
25
|
+
def initialize(expiration_time, cleaning_interval)
|
26
|
+
super()
|
27
|
+
@mutex = Mutex.new
|
28
|
+
@expiration_time = expiration_time
|
29
|
+
@cleaning_interval = cleaning_interval
|
30
|
+
@cache = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def set(key, value)
|
34
|
+
@mutex.synchronize do
|
35
|
+
start_cleaner_timer if @cleaning_interval.positive? && @cleaner_timer.nil?
|
36
|
+
@cache[key] = { value: value, expired: Time.now + @expiration_time }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def get(key)
|
41
|
+
entry = @cache[key]
|
42
|
+
return entry[:value] unless entry.nil? || expired?(entry)
|
43
|
+
|
44
|
+
@mutex.synchronize { @cache.delete(key) }
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def active_items
|
49
|
+
active_items = {}
|
50
|
+
remove_expired_entries
|
51
|
+
@mutex.synchronize do
|
52
|
+
@cache.each_pair { |key, entry| active_items[key] = entry[:value] }
|
53
|
+
end
|
54
|
+
active_items
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def start_cleaner_timer
|
60
|
+
@cleaner_timer = Concurrent::TimerTask.new(execution_interval: @cleaning_interval) do
|
61
|
+
remove_expired_entries
|
62
|
+
end
|
63
|
+
@cleaner_timer.execute
|
64
|
+
end
|
65
|
+
|
66
|
+
def stop_cleaner_timer
|
67
|
+
@cleaner_timer&.shutdown
|
68
|
+
@cleaner_timer = nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def remove_expired_entries
|
72
|
+
time = Time.now
|
73
|
+
@mutex.synchronize do
|
74
|
+
@cache.delete_if { |_, entry| expired?(entry, time) }
|
75
|
+
stop_cleaner_timer if @cache.empty?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def expired?(entry, time = Time.now)
|
80
|
+
entry[:expired] <= time
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent'
|
4
|
+
require_relative 'cache'
|
5
|
+
|
6
|
+
module Kameleoon
|
7
|
+
module Storage
|
8
|
+
# Will be useful for Ruby 3.0
|
9
|
+
# Abstract CacheFactory class (interface)
|
10
|
+
class CacheFactory
|
11
|
+
def create(_experiration_time, _cleaning_time)
|
12
|
+
raise 'Abstract method `create` called'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Implementation of CacheFactory with auto cleaning feature
|
17
|
+
class CacheFactoryImpl < CacheFactory
|
18
|
+
def create(expiration_time, cleaning_interval)
|
19
|
+
CacheImpl.new(expiration_time, cleaning_interval)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -8,21 +8,21 @@ module Kameleoon
|
|
8
8
|
|
9
9
|
def initialize(json_condition)
|
10
10
|
if json_condition['targetingType'].nil?
|
11
|
-
raise Exception::
|
11
|
+
raise Exception::NotFound.new('targetingType'), 'targetingType missed'
|
12
12
|
end
|
13
13
|
|
14
14
|
@type = json_condition['targetingType']
|
15
15
|
|
16
16
|
if json_condition['include'].nil? && json_condition['isInclude'].nil?
|
17
|
-
raise Exception::
|
17
|
+
raise Exception::NotFound.new('include / isInclude missed'), 'include / isInclude missed'
|
18
18
|
end
|
19
19
|
|
20
20
|
@include = json_condition['include'] || json_condition['isInclude']
|
21
21
|
end
|
22
22
|
|
23
23
|
def check(conditions)
|
24
|
-
raise
|
24
|
+
raise 'Abstract method `check` call'
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
28
|
-
end
|
28
|
+
end
|