kameleoon-client-ruby 3.22.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/configuration/custom_data_info.rb +1 -0
- data/lib/kameleoon/hybrid/manager.rb +1 -1
- data/lib/kameleoon/kameleoon_client.rb +0 -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/version.rb +1 -1
- metadata +2 -4
- data/lib/kameleoon/storage/cache.rb +0 -84
- data/lib/kameleoon/storage/cache_factory.rb +0 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 04efd3ab470f352395de3161c329086e07893f9a29d2e8d55085cc4ab00788c3
|
|
4
|
+
data.tar.gz: 62d498e4f19ec285358b29672d622e0476e70ae31cbf39bd480bf33dbe5b335b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0431f162cc71fb10db0c3920bef2a9d04c35713dfdfd467fc750ac16345c3cfd5965b8abb17c335ad8a7e7226d85c776a14d5d76030c7173969b00a61b309fd1
|
|
7
|
+
data.tar.gz: 683b9a9e65838df67344bc3b5ff05d8b04eb37fe5328ec4532f4d18b141d9957ca6709b3373adc22ce4f52791d3d0e90be8927693acfe159add2ba991812dc16
|
|
@@ -30,7 +30,6 @@ require 'kameleoon/network/network_manager'
|
|
|
30
30
|
require 'kameleoon/network/url_provider'
|
|
31
31
|
require 'kameleoon/network/cookie/cookie_manager'
|
|
32
32
|
require 'kameleoon/real_time/real_time_event_service'
|
|
33
|
-
require 'kameleoon/storage/cache_factory'
|
|
34
33
|
require 'kameleoon/targeting/models'
|
|
35
34
|
require 'kameleoon/targeting/targeting_manager'
|
|
36
35
|
require 'kameleoon/types/variable'
|
|
@@ -11,6 +11,7 @@ module Kameleoon
|
|
|
11
11
|
class TrackingManager
|
|
12
12
|
LINES_DELIMETER = "\n"
|
|
13
13
|
REQUEST_SIZE_LIMIT = 2560 * 1024 # 2.5 * 1024^2 characters
|
|
14
|
+
PROBE_MAX_VISITORS = 1
|
|
14
15
|
|
|
15
16
|
def initialize(
|
|
16
17
|
data_manager, network_manager, visitor_manager, track_interval_seconds, scheduler
|
|
@@ -19,7 +20,8 @@ module Kameleoon
|
|
|
19
20
|
'CALL: TrackingManager.new(data_manager, network_manager. visitor_manager, ' \
|
|
20
21
|
'track_interval_seconds: %s, scheduler)', track_interval_seconds
|
|
21
22
|
)
|
|
22
|
-
@tracking_visitors =
|
|
23
|
+
@tracking_visitors = ConcurrentVisitorTrackingRegistry.new(visitor_manager)
|
|
24
|
+
@probe_mode = false
|
|
23
25
|
@data_manager = data_manager
|
|
24
26
|
@network_manager = network_manager
|
|
25
27
|
@visitor_manager = visitor_manager
|
|
@@ -45,10 +47,19 @@ module Kameleoon
|
|
|
45
47
|
|
|
46
48
|
def track_all
|
|
47
49
|
Logging::KameleoonLogger.debug('CALL: TrackingManager.track_all')
|
|
48
|
-
|
|
50
|
+
if @probe_mode
|
|
51
|
+
track_probe
|
|
52
|
+
else
|
|
53
|
+
track(@tracking_visitors.extract)
|
|
54
|
+
end
|
|
49
55
|
Logging::KameleoonLogger.debug('RETURN: TrackingManager.track_all')
|
|
50
56
|
end
|
|
51
57
|
|
|
58
|
+
# Test purpose only
|
|
59
|
+
def probe_mode?
|
|
60
|
+
@probe_mode
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
def track_visitor(visitor_code)
|
|
53
64
|
Logging::KameleoonLogger.debug("CALL: TrackingManager.track_visitor(visitor_code: '%s')", visitor_code)
|
|
54
65
|
track([visitor_code])
|
|
@@ -57,6 +68,14 @@ module Kameleoon
|
|
|
57
68
|
|
|
58
69
|
private
|
|
59
70
|
|
|
71
|
+
# Sends one visitor's data; visitors without data are dropped (as on the normal path) until one is found.
|
|
72
|
+
def track_probe
|
|
73
|
+
until (visitor_code = @tracking_visitors.extract(PROBE_MAX_VISITORS)).empty?
|
|
74
|
+
return if track(visitor_code)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns whether a request was sent.
|
|
60
79
|
def track(visitor_codes)
|
|
61
80
|
builder = TrackingBuilder.new(visitor_codes, @data_manager.data_file, @visitor_manager, REQUEST_SIZE_LIMIT)
|
|
62
81
|
builder.build
|
|
@@ -71,8 +90,9 @@ module Kameleoon
|
|
|
71
90
|
perform_tracking_request(builder.visitor_codes_to_send, builder.unsent_visitor_data, builder.tracking_lines)
|
|
72
91
|
end
|
|
73
92
|
|
|
93
|
+
# Returns whether a request was sent.
|
|
74
94
|
def perform_tracking_request(visitor_codes, visitor_data, tracking_lines)
|
|
75
|
-
return if visitor_data.empty?
|
|
95
|
+
return false if visitor_data.empty?
|
|
76
96
|
|
|
77
97
|
lines = tracking_lines.join(LINES_DELIMETER)
|
|
78
98
|
# mark unsent data as transmitted
|
|
@@ -83,14 +103,32 @@ module Kameleoon
|
|
|
83
103
|
Logging::KameleoonLogger.info('Successful request for tracking visitors: %s, data: %s',
|
|
84
104
|
visitor_codes, visitor_data)
|
|
85
105
|
visitor_data.each { |d| d.mark_as_sent if d.is_a?(Kameleoon::Data) }
|
|
106
|
+
leave_probe_mode
|
|
86
107
|
else
|
|
87
108
|
Logging::KameleoonLogger.error('Tracking request failed')
|
|
88
109
|
Logging::KameleoonLogger.info('Failed request for tracking visitors: %s, data: %s',
|
|
89
110
|
visitor_codes, visitor_data)
|
|
90
111
|
visitor_data.each { |d| d.mark_as_unsent if d.is_a?(Kameleoon::Data) }
|
|
112
|
+
enter_probe_mode
|
|
91
113
|
@tracking_visitors.add_all(visitor_codes)
|
|
92
114
|
end
|
|
93
115
|
end
|
|
116
|
+
true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def enter_probe_mode
|
|
120
|
+
return if @probe_mode
|
|
121
|
+
|
|
122
|
+
@probe_mode = true
|
|
123
|
+
Logging::KameleoonLogger.info("Tracking request failed: only one visitor's data per tracking request " \
|
|
124
|
+
'will be sent until a request succeeds')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def leave_probe_mode
|
|
128
|
+
return unless @probe_mode
|
|
129
|
+
|
|
130
|
+
@probe_mode = false
|
|
131
|
+
Logging::KameleoonLogger.info('Tracking request succeeded: full-size tracking requests are restored')
|
|
94
132
|
end
|
|
95
133
|
end
|
|
96
134
|
end
|
|
@@ -1,91 +1,91 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'set'
|
|
4
|
-
|
|
5
3
|
module Kameleoon
|
|
6
4
|
module Managers
|
|
7
5
|
module Tracking
|
|
8
|
-
|
|
6
|
+
# Visitor codes waiting for a tracking request, in a Hash used as an insertion-ordered set.
|
|
7
|
+
#
|
|
8
|
+
# Lock-free on the hot path (MRI): `add` is a single Hash#[]=, which the GVL makes atomic (String keys hash
|
|
9
|
+
# in C, so no Ruby code runs inside it), and request threads never block on the registry. The Hash is never
|
|
10
|
+
# swapped out; `extract` removes with Hash#shift (O(1), oldest first) under a lock that only serializes
|
|
11
|
+
# extractors, so a concurrently added code is either returned now or kept for the next extraction, never
|
|
12
|
+
# lost. Nothing here iterates the live Hash: an `add` during iteration would raise in the request thread.
|
|
13
|
+
# Other engines run threads in parallel and take the lock on `add` as well: JRuby's Hash raises
|
|
14
|
+
# ConcurrencyError on unsynchronized concurrent writes; TruffleRuby's shared collections are thread-safe,
|
|
15
|
+
# but the lock-free path has not been measured there.
|
|
16
|
+
class ConcurrentVisitorTrackingRegistry
|
|
9
17
|
LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT = 2
|
|
10
18
|
REMOVAL_FACTOR = 0.8
|
|
19
|
+
UNLIMITED_EXTRACTION = 2**62
|
|
20
|
+
LOCK_FREE_ADD = (RUBY_ENGINE == 'ruby')
|
|
11
21
|
|
|
12
22
|
def initialize(visitor_manager, storage_limit = 1_000_000, extraction_limit = 20_000)
|
|
13
23
|
@visitor_manager = visitor_manager
|
|
14
24
|
@storage_limit = storage_limit
|
|
15
25
|
@extraction_limit = extraction_limit
|
|
16
|
-
@visitors =
|
|
17
|
-
@
|
|
26
|
+
@visitors = {}
|
|
27
|
+
@extract_mutex = Mutex.new
|
|
18
28
|
end
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
if LOCK_FREE_ADD
|
|
31
|
+
def add(visitor_code)
|
|
32
|
+
@visitors[visitor_code] = true
|
|
33
|
+
end
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@visitors.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
def add_all(visitor_codes)
|
|
36
|
+
visitor_codes.each { |vc| @visitors[vc] = true }
|
|
37
|
+
erase_to_storage_limit if @visitors.size > @storage_limit
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
def add(visitor_code)
|
|
41
|
+
@extract_mutex.synchronize { @visitors[visitor_code] = true }
|
|
31
42
|
end
|
|
43
|
+
|
|
44
|
+
def add_all(visitor_codes)
|
|
45
|
+
@extract_mutex.synchronize { visitor_codes.each { |vc| @visitors[vc] = true } }
|
|
46
|
+
erase_to_storage_limit if @visitors.size > @storage_limit # takes the (non-reentrant) mutex itself
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Removes and returns visitor codes, oldest first: all of them while the registry holds no more than
|
|
51
|
+
# `limit` codes and fewer than twice the extraction limit, otherwise `min(limit, extraction_limit)`.
|
|
52
|
+
def extract(limit = UNLIMITED_EXTRACTION)
|
|
53
|
+
@extract_mutex.synchronize { shift_visitors(extraction_count(@visitors.size, limit)) }
|
|
32
54
|
end
|
|
33
55
|
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
# Test purpose only
|
|
57
|
+
def visitors
|
|
58
|
+
@visitors.keys
|
|
36
59
|
end
|
|
37
60
|
|
|
38
61
|
private
|
|
39
62
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
end
|
|
63
|
+
def extraction_count(size, limit)
|
|
64
|
+
return size if (size <= limit) && (size < @extraction_limit * LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT)
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
old_visitors = nil
|
|
46
|
-
new_visitors = Set.new
|
|
47
|
-
@mutex.synchronize do
|
|
48
|
-
old_visitors = @visitors
|
|
49
|
-
@visitors = new_visitors
|
|
50
|
-
end
|
|
51
|
-
old_visitors
|
|
66
|
+
[limit, @extraction_limit].min
|
|
52
67
|
end
|
|
53
68
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
end
|
|
61
|
-
i = 0
|
|
62
|
-
extracted = []
|
|
63
|
-
@visitors.each do |vc|
|
|
64
|
-
break if i >= @extraction_limit
|
|
69
|
+
# Fewer than `count` codes come back only if the registry runs empty meanwhile.
|
|
70
|
+
def shift_visitors(count)
|
|
71
|
+
extracted = []
|
|
72
|
+
while extracted.size < count
|
|
73
|
+
pair = @visitors.shift
|
|
74
|
+
break if pair.nil?
|
|
65
75
|
|
|
66
|
-
|
|
67
|
-
i += 1
|
|
68
|
-
@visitors.delete(vc)
|
|
69
|
-
end
|
|
76
|
+
extracted.push(pair[0])
|
|
70
77
|
end
|
|
71
78
|
extracted
|
|
72
79
|
end
|
|
73
80
|
|
|
74
|
-
# Not thread-safe
|
|
75
|
-
def erase_nonexistent_visitors
|
|
76
|
-
@visitors.delete_if { |vc| @visitor_manager.get_visitor(vc).nil? }
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Not thread-safe
|
|
80
81
|
def erase_to_storage_limit
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
visitors_to_remove_count
|
|
88
|
-
@visitors.delete(vc)
|
|
82
|
+
@extract_mutex.synchronize do
|
|
83
|
+
# Drain and re-insert instead of `delete_if`, so that concurrent adds never hit an iteration in progress.
|
|
84
|
+
# Codes added while draining stay in the live Hash.
|
|
85
|
+
drained = shift_visitors(@visitors.size)
|
|
86
|
+
drained.each { |vc| @visitors[vc] = true unless @visitor_manager.peek_visitor(vc).nil? }
|
|
87
|
+
visitors_to_remove_count = @visitors.size - (@storage_limit * REMOVAL_FACTOR).to_i
|
|
88
|
+
visitors_to_remove_count.times { @visitors.shift }
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
end
|
|
@@ -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
|
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.22.
|
|
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
|
|
@@ -155,8 +155,6 @@ files:
|
|
|
155
155
|
- lib/kameleoon/real_time/sse_message.rb
|
|
156
156
|
- lib/kameleoon/real_time/sse_request.rb
|
|
157
157
|
- lib/kameleoon/sem_version.rb
|
|
158
|
-
- lib/kameleoon/storage/cache.rb
|
|
159
|
-
- lib/kameleoon/storage/cache_factory.rb
|
|
160
158
|
- lib/kameleoon/targeting/condition.rb
|
|
161
159
|
- lib/kameleoon/targeting/condition_factory.rb
|
|
162
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
|