kameleoon-client-ruby 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|