kameleoon-client-ruby 3.21.0 → 3.22.1
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_readiness.rb +119 -31
- data/lib/kameleoon/configuration/custom_data_info.rb +1 -0
- data/lib/kameleoon/events/data_file_update_event.rb +37 -0
- data/lib/kameleoon/events/data_file_update_handler.rb +22 -0
- data/lib/kameleoon/events/event_handler.rb +14 -0
- data/lib/kameleoon/events/event_manager.rb +79 -0
- data/lib/kameleoon/events/event_type.rb +17 -0
- data/lib/kameleoon/events/http_request_failure.rb +63 -0
- data/lib/kameleoon/events/http_request_handler.rb +32 -0
- data/lib/kameleoon/events/request_type.rb +29 -0
- data/lib/kameleoon/exceptions.rb +16 -0
- data/lib/kameleoon/hybrid/manager.rb +1 -1
- data/lib/kameleoon/kameleoon_client.rb +140 -65
- data/lib/kameleoon/kameleoon_client_factory.rb +1 -1
- data/lib/kameleoon/managers/tracking/tracking_manager.rb +41 -3
- data/lib/kameleoon/managers/tracking/visitor_tracking_registry.rb +57 -57
- data/lib/kameleoon/network/access_token_source.rb +126 -52
- data/lib/kameleoon/network/network_manager.rb +33 -10
- data/lib/kameleoon/real_time/{real_time_configuration_service.rb → real_time_event_service.rb} +16 -7
- data/lib/kameleoon/targeting/conditions/unknown_condition.rb +3 -3
- data/lib/kameleoon/version.rb +1 -1
- metadata +11 -5
- data/lib/kameleoon/storage/cache.rb +0 -84
- data/lib/kameleoon/storage/cache_factory.rb +0 -23
|
@@ -6,10 +6,12 @@ require 'time'
|
|
|
6
6
|
require 'kameleoon/utils'
|
|
7
7
|
require 'kameleoon/logging/kameleoon_logger'
|
|
8
8
|
require 'base64'
|
|
9
|
+
require 'concurrent'
|
|
9
10
|
|
|
10
11
|
module Kameleoon
|
|
11
12
|
module Network
|
|
12
13
|
class AccessTokenSource
|
|
14
|
+
SILENCE_PERIOD = 300 # in seconds
|
|
13
15
|
TOKEN_EXPIRATION_GAP = 60 # in seconds
|
|
14
16
|
TOKEN_OBSOLESCENCE_GAP = 1800 # in seconds
|
|
15
17
|
JWT_ACCESS_TOKEN_FIELD = 'access_token'
|
|
@@ -24,7 +26,10 @@ module Kameleoon
|
|
|
24
26
|
@network_manager = network_manager
|
|
25
27
|
@client_id = client_id
|
|
26
28
|
@client_secret = client_secret
|
|
27
|
-
@
|
|
29
|
+
@cached_token = nil
|
|
30
|
+
@silent_after_fetch_failure_until = 0.0 # 0 = never silenced
|
|
31
|
+
@fetching = nil # Concurrent::Event of the in-flight fetch, shared by all concurrent callers; nil when idle
|
|
32
|
+
@fetching_mutex = Mutex.new
|
|
28
33
|
@basic_auth_token = AccessTokenSource.construct_basic_token(client_id, client_secret)
|
|
29
34
|
Logging::KameleoonLogger.debug(lambda {
|
|
30
35
|
format("RETURN: AccessTokenSource.new(network_manager, client_id: '%s', client_secret: '%s')",
|
|
@@ -37,16 +42,24 @@ module Kameleoon
|
|
|
37
42
|
"#{BASIC_AUTHORIZATION_PREFIX}#{Base64.strict_encode64(basic_token_content)}"
|
|
38
43
|
end
|
|
39
44
|
|
|
45
|
+
# Returns the access token, or `nil` if it cannot be obtained. Fetches a new token if the cached one is
|
|
46
|
+
# expired or obsolete, unless a recent fetch failure has put the source in the silence period.
|
|
40
47
|
def get_token(timeout = nil)
|
|
41
|
-
Logging::KameleoonLogger.debug('CALL: AccessTokenSource.
|
|
42
|
-
now =
|
|
48
|
+
Logging::KameleoonLogger.debug('CALL: AccessTokenSource.get_token(timeout: %s)', timeout)
|
|
49
|
+
now = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
43
50
|
token = @cached_token
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
silent = now < @silent_after_fetch_failure_until
|
|
52
|
+
if !token.nil? && !token.expired?(now)
|
|
53
|
+
refresh_token_in_background(timeout) if token.obsolete?(now) && !silent
|
|
54
|
+
value = token.value
|
|
55
|
+
elsif silent
|
|
56
|
+
value = nil
|
|
57
|
+
else
|
|
58
|
+
value = fetch_token(timeout)
|
|
59
|
+
end
|
|
60
|
+
Logging::KameleoonLogger.debug("RETURN: AccessTokenSource.get_token(timeout: %s) -> (token: '%s')",
|
|
61
|
+
timeout, value)
|
|
62
|
+
value
|
|
50
63
|
end
|
|
51
64
|
|
|
52
65
|
def discard_token(token)
|
|
@@ -58,54 +71,114 @@ module Kameleoon
|
|
|
58
71
|
|
|
59
72
|
private
|
|
60
73
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
nil
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Logging::KameleoonLogger.error("Failed to run access token fetching: #{e}")
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def fetch_token(timeout = nil)
|
|
80
|
-
Logging::KameleoonLogger.debug('CALL: AccessTokenSource.fetch_token(timeout: %s)', timeout)
|
|
81
|
-
response_content = @network_manager.fetch_access_jwtoken(@basic_auth_token, timeout)
|
|
82
|
-
unless response_content
|
|
83
|
-
Logging::KameleoonLogger.error('Failed to fetch access JWT')
|
|
84
|
-
return nil
|
|
74
|
+
NO_FETCH = [nil, false].freeze
|
|
75
|
+
|
|
76
|
+
# The first caller performs the request; concurrent callers wait for it - no longer than their own request
|
|
77
|
+
# timeout - and then read the cached token.
|
|
78
|
+
def fetch_token(timeout)
|
|
79
|
+
fetching, owner = claim_fetch(false)
|
|
80
|
+
if fetching.nil?
|
|
81
|
+
cached_token_value
|
|
82
|
+
elsif owner
|
|
83
|
+
perform_fetch(fetching, timeout)
|
|
84
|
+
else
|
|
85
|
+
unless fetching.wait(timeout.nil? ? nil : timeout / 1000.0)
|
|
86
|
+
Logging::KameleoonLogger.debug('Access token fetch has not completed within %s ms', timeout)
|
|
87
|
+
end
|
|
88
|
+
cached_token_value
|
|
85
89
|
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Refreshes a valid but obsolete token while it keeps being served.
|
|
93
|
+
def refresh_token_in_background(timeout)
|
|
94
|
+
fetching, owner = claim_fetch(true)
|
|
95
|
+
Thread.new { perform_fetch(fetching, timeout) } if owner
|
|
96
|
+
rescue StandardError => e # e.g. ThreadError: the thread limit is reached
|
|
97
|
+
enter_silence(e)
|
|
98
|
+
complete_fetch(fetching)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Returns `[event, owner]`: the event of the in-flight fetch and whether the caller has just registered it
|
|
102
|
+
# (and so must perform it); NO_FETCH when no request is needed anymore.
|
|
103
|
+
def claim_fetch(refresh)
|
|
104
|
+
@fetching_mutex.synchronize do
|
|
105
|
+
return [@fetching, false] unless @fetching.nil?
|
|
106
|
+
# get_token decided without the mutex; meanwhile another caller's fetch may have cached a token, or failed
|
|
107
|
+
# and started the silence period.
|
|
108
|
+
return NO_FETCH unless fetch_needed?(refresh)
|
|
109
|
+
|
|
110
|
+
@fetching = Concurrent::Event.new
|
|
111
|
+
[@fetching, true]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# `refresh`: the caller holds a valid but obsolete token, which is worth a request only while still obsolete.
|
|
116
|
+
def fetch_needed?(refresh)
|
|
117
|
+
now = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
118
|
+
return false if now < @silent_after_fetch_failure_until
|
|
119
|
+
|
|
120
|
+
token = @cached_token
|
|
121
|
+
token.nil? || token.expired?(now) || (refresh && token.obsolete?(now))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def cached_token_value
|
|
125
|
+
token = @cached_token
|
|
126
|
+
token.nil? || token.expired?(Process.clock_gettime(Process::CLOCK_REALTIME)) ? nil : token.value
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def perform_fetch(fetching, timeout)
|
|
130
|
+
Logging::KameleoonLogger.debug('CALL: AccessTokenSource.perform_fetch(timeout: %s)', timeout)
|
|
131
|
+
token = nil
|
|
86
132
|
begin
|
|
133
|
+
response_content = @network_manager.fetch_access_jwtoken(@basic_auth_token, timeout)
|
|
134
|
+
raise FetchError, 'access token request failed' unless response_content
|
|
135
|
+
|
|
87
136
|
jwt = JSON.parse(response_content)
|
|
88
|
-
token
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
137
|
+
raise FetchError, "access token response is not a JSON object: '#{jwt}'" unless jwt.is_a?(Hash)
|
|
138
|
+
|
|
139
|
+
expires_in = read_expires_in(jwt)
|
|
140
|
+
token = read_access_token(jwt)
|
|
141
|
+
@cached_token = new_expiring_token(token, expires_in)
|
|
142
|
+
Logging::KameleoonLogger.info('Fetched access token')
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
token = nil
|
|
145
|
+
enter_silence(e)
|
|
146
|
+
ensure
|
|
147
|
+
complete_fetch(fetching) # also on non-StandardError, so that waiters are never stuck
|
|
97
148
|
end
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"RETURN: AccessTokenSource.fetch_token(timeout: %s) -> (token: '%s')", timeout, token
|
|
101
|
-
)
|
|
149
|
+
Logging::KameleoonLogger.debug("RETURN: AccessTokenSource.perform_fetch(timeout: %s) -> (token: '%s')",
|
|
150
|
+
timeout, token)
|
|
102
151
|
token
|
|
103
|
-
ensure
|
|
104
|
-
@fetching = false
|
|
105
152
|
end
|
|
106
153
|
|
|
107
|
-
def
|
|
108
|
-
|
|
154
|
+
def enter_silence(error)
|
|
155
|
+
@silent_after_fetch_failure_until = Process.clock_gettime(Process::CLOCK_REALTIME) + SILENCE_PERIOD
|
|
156
|
+
Logging::KameleoonLogger.warning('Failed to fetch access token (%s: %s); it will not be requested for %ds',
|
|
157
|
+
error.class, error.message, SILENCE_PERIOD)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def complete_fetch(fetching)
|
|
161
|
+
# Clear before waking the waiters so that a waiter re-entering get_token can start a new fetch.
|
|
162
|
+
@fetching_mutex.synchronize { @fetching = nil if @fetching.equal?(fetching) }
|
|
163
|
+
fetching.set
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def read_expires_in(jwt)
|
|
167
|
+
expires_in = jwt[JWT_EXPIRES_IN_FIELD]
|
|
168
|
+
return expires_in if expires_in.is_a?(Integer) && expires_in.positive?
|
|
169
|
+
|
|
170
|
+
raise FetchError, "access token response has no valid '#{JWT_EXPIRES_IN_FIELD}' field: '#{expires_in}'"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def read_access_token(jwt)
|
|
174
|
+
token = jwt[JWT_ACCESS_TOKEN_FIELD]
|
|
175
|
+
return token if token.is_a?(String) && !token.empty?
|
|
176
|
+
|
|
177
|
+
raise FetchError, "access token response has no valid '#{JWT_ACCESS_TOKEN_FIELD}' field"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def new_expiring_token(token, expires_in)
|
|
181
|
+
now = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
109
182
|
exp_time = now + expires_in - TOKEN_EXPIRATION_GAP
|
|
110
183
|
if expires_in > TOKEN_OBSOLESCENCE_GAP
|
|
111
184
|
obs_time = now + expires_in - TOKEN_OBSOLESCENCE_GAP
|
|
@@ -121,9 +194,10 @@ module Kameleoon
|
|
|
121
194
|
)
|
|
122
195
|
end
|
|
123
196
|
end
|
|
124
|
-
|
|
125
|
-
token
|
|
197
|
+
ExpiringToken.new(token, exp_time, obs_time)
|
|
126
198
|
end
|
|
199
|
+
|
|
200
|
+
class FetchError < StandardError; end
|
|
127
201
|
end
|
|
128
202
|
|
|
129
203
|
class ExpiringToken
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'kameleoon/events/event_manager'
|
|
3
4
|
require 'kameleoon/logging/kameleoon_logger'
|
|
4
5
|
require 'kameleoon/network/content_type'
|
|
5
6
|
require 'kameleoon/network/fetched_configuration'
|
|
@@ -14,8 +15,8 @@ module Kameleoon
|
|
|
14
15
|
##
|
|
15
16
|
# NetworkManager is used to make API calls.
|
|
16
17
|
class NetworkManager
|
|
17
|
-
FETCH_CONFIGURATION_ATTEMPT_NUMBER =
|
|
18
|
-
TRACKING_CALL_ATTEMPT_NUMBER =
|
|
18
|
+
FETCH_CONFIGURATION_ATTEMPT_NUMBER = 2
|
|
19
|
+
TRACKING_CALL_ATTEMPT_NUMBER = 2
|
|
19
20
|
TRACKING_CALL_RETRY_DELAY = 5.0 # in seconds
|
|
20
21
|
SDK_TYPE_HEADER = 'X-Kameleoon-SDK-Type'
|
|
21
22
|
SDK_VERSION_HEADER = 'X-Kameleoon-SDK-Version'
|
|
@@ -23,13 +24,14 @@ module Kameleoon
|
|
|
23
24
|
HEADER_IF_MODIFIED_SINCE = 'If-Modified-Since'
|
|
24
25
|
HEADER_LAST_MODIFIED = 'last-modified' # in lower case because the network lib casts response headers to lower
|
|
25
26
|
|
|
26
|
-
attr_reader :environment, :default_timeout, :access_token_source, :url_provider
|
|
27
|
+
attr_reader :environment, :default_timeout, :access_token_source, :url_provider, :event_manager
|
|
27
28
|
|
|
28
|
-
def initialize(environment, default_timeout, access_token_source_factory, url_provider)
|
|
29
|
+
def initialize(environment, default_timeout, access_token_source_factory, url_provider, event_manager)
|
|
29
30
|
@environment = environment
|
|
30
31
|
@default_timeout = default_timeout
|
|
31
32
|
@access_token_source = access_token_source_factory.create(self)
|
|
32
33
|
@url_provider = url_provider
|
|
34
|
+
@event_manager = event_manager
|
|
33
35
|
@sync_net_provider = SyncNetProvider.new
|
|
34
36
|
end
|
|
35
37
|
|
|
@@ -39,7 +41,8 @@ module Kameleoon
|
|
|
39
41
|
headers = { SDK_TYPE_HEADER => SDK_NAME, SDK_VERSION_HEADER => SDK_VERSION }
|
|
40
42
|
headers[HEADER_IF_MODIFIED_SINCE] = if_modified_since if if_modified_since
|
|
41
43
|
request = Request.new(Method::GET, url, ContentType::JSON, timeout, extra_headers: headers)
|
|
42
|
-
response, success = make_call(request, false,
|
|
44
|
+
response, success = make_call(Events::RequestType::DATAFILE, request, false,
|
|
45
|
+
FETCH_CONFIGURATION_ATTEMPT_NUMBER - 1)
|
|
43
46
|
return nil unless success
|
|
44
47
|
return FetchedConfiguration.new(nil, nil) if response.code == 304
|
|
45
48
|
|
|
@@ -51,14 +54,14 @@ module Kameleoon
|
|
|
51
54
|
url = @url_provider.make_api_data_get_request_url(key)
|
|
52
55
|
timeout = ensure_timeout(timeout)
|
|
53
56
|
request = Request.new(Method::GET, url, ContentType::JSON, timeout)
|
|
54
|
-
unwrap_response(*make_call(request, true))
|
|
57
|
+
unwrap_response(*make_call(Events::RequestType::REMOTE_DATA, request, true))
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
def get_remote_visitor_data(visitor_code, filter, is_unique_identifier, timeout = nil)
|
|
58
61
|
url = @url_provider.make_visitor_data_get_url(visitor_code, filter, is_unique_identifier)
|
|
59
62
|
timeout = ensure_timeout(timeout)
|
|
60
63
|
request = Request.new(Method::GET, url, ContentType::JSON, timeout)
|
|
61
|
-
unwrap_response(*make_call(request, true))
|
|
64
|
+
unwrap_response(*make_call(Events::RequestType::REMOTE_VISITOR_DATA, request, true))
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
def send_tracking_data(lines, timeout = nil)
|
|
@@ -67,7 +70,8 @@ module Kameleoon
|
|
|
67
70
|
url = @url_provider.make_tracking_url
|
|
68
71
|
timeout = ensure_timeout(timeout)
|
|
69
72
|
request = Request.new(Method::POST, url, ContentType::WILDCARD, timeout, data: lines)
|
|
70
|
-
unwrap_response(*make_call(request, true,
|
|
73
|
+
unwrap_response(*make_call(Events::RequestType::TRACKING, request, true,
|
|
74
|
+
TRACKING_CALL_ATTEMPT_NUMBER - 1, TRACKING_CALL_RETRY_DELAY))
|
|
71
75
|
end
|
|
72
76
|
|
|
73
77
|
def fetch_access_jwtoken(basic_auth_token, timeout = nil)
|
|
@@ -79,12 +83,12 @@ module Kameleoon
|
|
|
79
83
|
data = UriHelper.encode_query(data_map).encode('UTF-8')
|
|
80
84
|
request = Request.new(Method::POST, url, ContentType::FORM, timeout, data: data)
|
|
81
85
|
request.authorize(basic_auth_token)
|
|
82
|
-
unwrap_response(*make_call(request, false))
|
|
86
|
+
unwrap_response(*make_call(Events::RequestType::ACCESS_TOKEN, request, false))
|
|
83
87
|
end
|
|
84
88
|
|
|
85
89
|
private
|
|
86
90
|
|
|
87
|
-
def make_call(request, try_access_token_auth, retry_limit = 0, retry_delay = 0)
|
|
91
|
+
def make_call(request_type, request, try_access_token_auth, retry_limit = 0, retry_delay = 0)
|
|
88
92
|
Logging::KameleoonLogger.debug('Running request %s with access token %s, retry limit %s, retry delay %s ms',
|
|
89
93
|
request, try_access_token_auth, retry_limit, retry_delay)
|
|
90
94
|
attempt = 0
|
|
@@ -93,7 +97,10 @@ module Kameleoon
|
|
|
93
97
|
while !success && (attempt <= retry_limit)
|
|
94
98
|
delay(retry_delay) if attempt.positive? && retry_delay.positive?
|
|
95
99
|
try_authorize(request) if try_access_token_auth
|
|
100
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
96
101
|
response = @sync_net_provider.make_request(request)
|
|
102
|
+
duration_millis = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_time).round
|
|
103
|
+
fire_http_request_event(request_type, response, duration_millis)
|
|
97
104
|
if response.success?
|
|
98
105
|
success = true
|
|
99
106
|
elsif !response.error.nil?
|
|
@@ -122,6 +129,22 @@ module Kameleoon
|
|
|
122
129
|
[response, success]
|
|
123
130
|
end
|
|
124
131
|
|
|
132
|
+
def fire_http_request_event(request_type, response, duration_millis)
|
|
133
|
+
if response.success?
|
|
134
|
+
@event_manager.fire_http_request_succeeded(request_type, response.code, duration_millis)
|
|
135
|
+
else
|
|
136
|
+
failure =
|
|
137
|
+
if response.error.nil?
|
|
138
|
+
Events::HttpRequestFailure.from_http_status(response.code)
|
|
139
|
+
elsif response.error.is_a?(Timeout::Error)
|
|
140
|
+
Events::HttpRequestFailure.of_cancellation
|
|
141
|
+
else
|
|
142
|
+
Events::HttpRequestFailure.from_exception(response.error)
|
|
143
|
+
end
|
|
144
|
+
@event_manager.fire_http_request_failed(request_type, failure, duration_millis)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
125
148
|
def get_log_level(attempt, attempt_count)
|
|
126
149
|
attempt < attempt_count ? Logging::LogLevel::WARNING : Logging::LogLevel::ERROR
|
|
127
150
|
end
|
data/lib/kameleoon/real_time/{real_time_configuration_service.rb → real_time_event_service.rb}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Kameleoon Real Time
|
|
3
|
+
# Kameleoon Real Time Event Service
|
|
4
4
|
|
|
5
5
|
require 'json'
|
|
6
6
|
require 'kameleoon/logging/kameleoon_logger'
|
|
@@ -10,19 +10,24 @@ require 'kameleoon/real_time/sse_client'
|
|
|
10
10
|
|
|
11
11
|
module Kameleoon
|
|
12
12
|
module RealTime
|
|
13
|
-
|
|
13
|
+
DATA_FILE_UPDATE_EVENT = 'configuration-update-event'
|
|
14
14
|
|
|
15
15
|
##
|
|
16
|
-
#
|
|
16
|
+
# RealTimeEventService is used for fetching updates of configuration
|
|
17
17
|
# (experiments and feature flags) in real time.
|
|
18
|
-
class
|
|
18
|
+
class RealTimeEventService
|
|
19
|
+
# Delay before re-establishing a failed SSE connection, so a persistent
|
|
20
|
+
# failure does not turn into a hot reconnect loop.
|
|
21
|
+
RECONNECT_DELAY_SECONDS = 3
|
|
22
|
+
|
|
19
23
|
##
|
|
20
24
|
# Parametrized initializer.
|
|
21
25
|
#
|
|
22
26
|
# @param url [String]
|
|
23
27
|
# @param update_handler [Callable[Kameleoon::RealTime::RealTimeEvent] | NilClass] Handler which
|
|
24
28
|
# is synchronously called for gotten RealTimeEvent objects.
|
|
25
|
-
|
|
29
|
+
# @param reconnect_delay [Numeric] Delay in seconds before a reconnect attempt.
|
|
30
|
+
def initialize(url, update_handler, sse_request_source = nil, reconnect_delay: RECONNECT_DELAY_SECONDS)
|
|
26
31
|
@url = url
|
|
27
32
|
@update_handler = update_handler
|
|
28
33
|
@need_close = false
|
|
@@ -32,6 +37,7 @@ module Kameleoon
|
|
|
32
37
|
'Connection': 'Keep-Alive'
|
|
33
38
|
}
|
|
34
39
|
@sse_request_source = sse_request_source
|
|
40
|
+
@reconnect_delay = reconnect_delay
|
|
35
41
|
@sse_thread = nil
|
|
36
42
|
@sse_client = nil
|
|
37
43
|
create_sse_client
|
|
@@ -42,7 +48,7 @@ module Kameleoon
|
|
|
42
48
|
def close
|
|
43
49
|
return if @need_close
|
|
44
50
|
|
|
45
|
-
Logging::KameleoonLogger.info('Real-time
|
|
51
|
+
Logging::KameleoonLogger.info('Real-time event service is shutting down')
|
|
46
52
|
@need_close = true
|
|
47
53
|
return if @sse_thread.nil?
|
|
48
54
|
|
|
@@ -60,7 +66,7 @@ module Kameleoon
|
|
|
60
66
|
def init_sse_client
|
|
61
67
|
message_handler = proc do |message|
|
|
62
68
|
Logging::KameleoonLogger.debug("Got SSE event: #{message.event}")
|
|
63
|
-
if message.event ==
|
|
69
|
+
if message.event == DATA_FILE_UPDATE_EVENT
|
|
64
70
|
event_dict = JSON.parse(message.data)
|
|
65
71
|
@update_handler&.call(RealTimeEvent.new(event_dict))
|
|
66
72
|
end
|
|
@@ -90,6 +96,9 @@ module Kameleoon
|
|
|
90
96
|
rescue StandardError => e
|
|
91
97
|
Logging::KameleoonLogger.error("Error occurred within SSE client: #{e}")
|
|
92
98
|
end
|
|
99
|
+
# The delay is applied to every re-establishment (a failed connection and
|
|
100
|
+
# a dropped stream alike), so a persistent failure cannot produce a hot loop.
|
|
101
|
+
sleep(@reconnect_delay) unless @need_close
|
|
93
102
|
end
|
|
94
103
|
end
|
|
95
104
|
end
|
|
@@ -5,11 +5,11 @@ require 'kameleoon/targeting/condition'
|
|
|
5
5
|
module Kameleoon
|
|
6
6
|
# @api private
|
|
7
7
|
module Targeting
|
|
8
|
-
# UnknownCondition represents not defined condition, always returns that visitor is targeted (
|
|
8
|
+
# UnknownCondition represents not defined condition, always returns that visitor is not targeted (false)
|
|
9
9
|
class UnknownCondition < Condition
|
|
10
10
|
def check(_data)
|
|
11
|
-
Logging::KameleoonLogger.warning('Condition of unknown type \'%s\' evaluated as
|
|
12
|
-
|
|
11
|
+
Logging::KameleoonLogger.warning('Condition of unknown type \'%s\' evaluated as false', type)
|
|
12
|
+
false
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
end
|
data/lib/kameleoon/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kameleoon-client-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.22.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kameleoon
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: em-http-request
|
|
@@ -114,6 +114,14 @@ files:
|
|
|
114
114
|
- lib/kameleoon/data/unique_identifier.rb
|
|
115
115
|
- lib/kameleoon/data/user_agent.rb
|
|
116
116
|
- lib/kameleoon/data/visitor_visits.rb
|
|
117
|
+
- lib/kameleoon/events/data_file_update_event.rb
|
|
118
|
+
- lib/kameleoon/events/data_file_update_handler.rb
|
|
119
|
+
- lib/kameleoon/events/event_handler.rb
|
|
120
|
+
- lib/kameleoon/events/event_manager.rb
|
|
121
|
+
- lib/kameleoon/events/event_type.rb
|
|
122
|
+
- lib/kameleoon/events/http_request_failure.rb
|
|
123
|
+
- lib/kameleoon/events/http_request_handler.rb
|
|
124
|
+
- lib/kameleoon/events/request_type.rb
|
|
117
125
|
- lib/kameleoon/exceptions.rb
|
|
118
126
|
- lib/kameleoon/hybrid/manager.rb
|
|
119
127
|
- lib/kameleoon/kameleoon_client.rb
|
|
@@ -141,14 +149,12 @@ files:
|
|
|
141
149
|
- lib/kameleoon/network/response.rb
|
|
142
150
|
- lib/kameleoon/network/uri_helper.rb
|
|
143
151
|
- lib/kameleoon/network/url_provider.rb
|
|
144
|
-
- lib/kameleoon/real_time/real_time_configuration_service.rb
|
|
145
152
|
- lib/kameleoon/real_time/real_time_event.rb
|
|
153
|
+
- lib/kameleoon/real_time/real_time_event_service.rb
|
|
146
154
|
- lib/kameleoon/real_time/sse_client.rb
|
|
147
155
|
- lib/kameleoon/real_time/sse_message.rb
|
|
148
156
|
- lib/kameleoon/real_time/sse_request.rb
|
|
149
157
|
- lib/kameleoon/sem_version.rb
|
|
150
|
-
- lib/kameleoon/storage/cache.rb
|
|
151
|
-
- lib/kameleoon/storage/cache_factory.rb
|
|
152
158
|
- lib/kameleoon/targeting/condition.rb
|
|
153
159
|
- lib/kameleoon/targeting/condition_factory.rb
|
|
154
160
|
- lib/kameleoon/targeting/conditions/browser_condition.rb
|
|
@@ -1,84 +0,0 @@
|
|
|
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
|
|
@@ -1,23 +0,0 @@
|
|
|
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
|