kameleoon-client-ruby 3.22.0 → 3.22.2
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 +1 -1
- data/lib/kameleoon/managers/tracking/tracking_manager.rb +85 -17
- 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/net_provider.rb +92 -10
- data/lib/kameleoon/network/network_manager.rb +6 -0
- data/lib/kameleoon/real_time/sse_request.rb +6 -3
- data/lib/kameleoon/version.rb +1 -1
- metadata +10 -26
- 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: 7eceff94d1ab7a39dfa3603c7cfd124bad0c49a7315feb7e0bd938c45feca9f0
|
|
4
|
+
data.tar.gz: f3df43034ca7460e3fe21a47caa920e9ede1e65ac49cb5c04b0196198d4a2391
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ed0b76309075aac725c827733a454754f6183001a600582f79e116c8774d5ac4e8448dbb7b58fcb006649d1801cc0d86ef0484b0e6ee70201d4eb6cf3aa92fd
|
|
7
|
+
data.tar.gz: 927ca5a73f64293a53b3f4e0083112ed0257de431c04fa80541102cd1fec954d80c122c9138448264090e017d78b02a8b1028ac4ed36d1e1ecb0b489b7822211
|
|
@@ -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'
|
|
@@ -1034,6 +1033,7 @@ module Kameleoon
|
|
|
1034
1033
|
@visitor_manager.stop
|
|
1035
1034
|
@tracking_manager.stop
|
|
1036
1035
|
@scheduler.shutdown
|
|
1036
|
+
@network_manager.close
|
|
1037
1037
|
Logging::KameleoonLogger.debug('RETURN: KameleoonClient.dispose')
|
|
1038
1038
|
end
|
|
1039
1039
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'concurrent/executor/thread_pool_executor'
|
|
3
4
|
require 'kameleoon/data/data'
|
|
4
5
|
require 'kameleoon/logging/kameleoon_logger'
|
|
5
6
|
require 'kameleoon/managers/tracking/tracking_builder'
|
|
@@ -11,18 +12,28 @@ module Kameleoon
|
|
|
11
12
|
class TrackingManager
|
|
12
13
|
LINES_DELIMETER = "\n"
|
|
13
14
|
REQUEST_SIZE_LIMIT = 2560 * 1024 # 2.5 * 1024^2 characters
|
|
15
|
+
PROBE_MAX_VISITORS = 1
|
|
16
|
+
MAX_SEND_THREADS = 8
|
|
17
|
+
MAX_QUEUED_REQUESTS = 32
|
|
18
|
+
SEND_THREAD_IDLE_SECONDS = 60
|
|
19
|
+
STOP_WAIT_SECONDS = 1
|
|
14
20
|
|
|
15
21
|
def initialize(
|
|
16
|
-
data_manager, network_manager, visitor_manager, track_interval_seconds, scheduler
|
|
22
|
+
data_manager, network_manager, visitor_manager, track_interval_seconds, scheduler, executor: nil
|
|
17
23
|
)
|
|
18
24
|
Logging::KameleoonLogger.debug(
|
|
19
25
|
'CALL: TrackingManager.new(data_manager, network_manager. visitor_manager, ' \
|
|
20
26
|
'track_interval_seconds: %s, scheduler)', track_interval_seconds
|
|
21
27
|
)
|
|
22
|
-
@tracking_visitors =
|
|
28
|
+
@tracking_visitors = ConcurrentVisitorTrackingRegistry.new(visitor_manager)
|
|
29
|
+
@probe_mode = false
|
|
23
30
|
@data_manager = data_manager
|
|
24
31
|
@network_manager = network_manager
|
|
25
32
|
@visitor_manager = visitor_manager
|
|
33
|
+
@executor = executor || Concurrent::ThreadPoolExecutor.new(
|
|
34
|
+
name: 'kameleoon-tracking', min_threads: 0, max_threads: MAX_SEND_THREADS,
|
|
35
|
+
max_queue: MAX_QUEUED_REQUESTS, idletime: SEND_THREAD_IDLE_SECONDS, fallback_policy: :abort
|
|
36
|
+
)
|
|
26
37
|
@track_all_job = scheduler.schedule_every(track_interval_seconds, method(:track_all)) unless scheduler.nil?
|
|
27
38
|
Logging::KameleoonLogger.debug(
|
|
28
39
|
'RETURN: TrackingManager.new(data_manager, network_manager. visitor_manager, ' \
|
|
@@ -34,6 +45,10 @@ module Kameleoon
|
|
|
34
45
|
Logging::KameleoonLogger.debug('CALL: TrackingManager.stop')
|
|
35
46
|
@track_all_job&.unschedule
|
|
36
47
|
@track_all_job = nil
|
|
48
|
+
# Queued requests still run; a later `track` is rejected and keeps its data (see `perform_tracking_request`).
|
|
49
|
+
# The wait lets in-flight requests finish before the client disposal closes the connections.
|
|
50
|
+
@executor.shutdown
|
|
51
|
+
@executor.wait_for_termination(STOP_WAIT_SECONDS)
|
|
37
52
|
Logging::KameleoonLogger.debug('RETURN: TrackingManager.stop')
|
|
38
53
|
end
|
|
39
54
|
|
|
@@ -45,10 +60,19 @@ module Kameleoon
|
|
|
45
60
|
|
|
46
61
|
def track_all
|
|
47
62
|
Logging::KameleoonLogger.debug('CALL: TrackingManager.track_all')
|
|
48
|
-
|
|
63
|
+
if @probe_mode
|
|
64
|
+
track_probe
|
|
65
|
+
else
|
|
66
|
+
track(@tracking_visitors.extract)
|
|
67
|
+
end
|
|
49
68
|
Logging::KameleoonLogger.debug('RETURN: TrackingManager.track_all')
|
|
50
69
|
end
|
|
51
70
|
|
|
71
|
+
# Test purpose only
|
|
72
|
+
def probe_mode?
|
|
73
|
+
@probe_mode
|
|
74
|
+
end
|
|
75
|
+
|
|
52
76
|
def track_visitor(visitor_code)
|
|
53
77
|
Logging::KameleoonLogger.debug("CALL: TrackingManager.track_visitor(visitor_code: '%s')", visitor_code)
|
|
54
78
|
track([visitor_code])
|
|
@@ -57,6 +81,14 @@ module Kameleoon
|
|
|
57
81
|
|
|
58
82
|
private
|
|
59
83
|
|
|
84
|
+
# Sends one visitor's data; visitors without data are dropped (as on the normal path) until one is found.
|
|
85
|
+
def track_probe
|
|
86
|
+
until (visitor_code = @tracking_visitors.extract(PROBE_MAX_VISITORS)).empty?
|
|
87
|
+
return if track(visitor_code)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns whether a request was sent.
|
|
60
92
|
def track(visitor_codes)
|
|
61
93
|
builder = TrackingBuilder.new(visitor_codes, @data_manager.data_file, @visitor_manager, REQUEST_SIZE_LIMIT)
|
|
62
94
|
builder.build
|
|
@@ -71,26 +103,62 @@ module Kameleoon
|
|
|
71
103
|
perform_tracking_request(builder.visitor_codes_to_send, builder.unsent_visitor_data, builder.tracking_lines)
|
|
72
104
|
end
|
|
73
105
|
|
|
106
|
+
# Returns whether a request was sent.
|
|
74
107
|
def perform_tracking_request(visitor_codes, visitor_data, tracking_lines)
|
|
75
|
-
return if visitor_data.empty?
|
|
108
|
+
return false if visitor_data.empty?
|
|
76
109
|
|
|
77
110
|
lines = tracking_lines.join(LINES_DELIMETER)
|
|
78
111
|
# mark unsent data as transmitted
|
|
79
112
|
visitor_data.each { |d| d.mark_as_transmitting if d.is_a?(Kameleoon::Data) }
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
113
|
+
begin
|
|
114
|
+
@executor.post { send_tracking_request(visitor_codes, visitor_data, lines) }
|
|
115
|
+
rescue Concurrent::RejectedExecutionError
|
|
116
|
+
Logging::KameleoonLogger.warning(
|
|
117
|
+
'Tracking request pool is full or stopped: data of %s visitors is kept to be sent later',
|
|
118
|
+
visitor_codes.size
|
|
119
|
+
)
|
|
120
|
+
keep_for_later(visitor_codes, visitor_data)
|
|
121
|
+
end
|
|
122
|
+
true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def send_tracking_request(visitor_codes, visitor_data, lines)
|
|
126
|
+
result = @network_manager.send_tracking_data(lines)
|
|
127
|
+
if result != false
|
|
128
|
+
Logging::KameleoonLogger.info('Successful request for tracking visitors: %s, data: %s',
|
|
129
|
+
visitor_codes, visitor_data)
|
|
130
|
+
visitor_data.each { |d| d.mark_as_sent if d.is_a?(Kameleoon::Data) }
|
|
131
|
+
leave_probe_mode
|
|
132
|
+
else
|
|
133
|
+
Logging::KameleoonLogger.error('Tracking request failed')
|
|
134
|
+
Logging::KameleoonLogger.info('Failed request for tracking visitors: %s, data: %s',
|
|
135
|
+
visitor_codes, visitor_data)
|
|
136
|
+
enter_probe_mode
|
|
137
|
+
keep_for_later(visitor_codes, visitor_data)
|
|
93
138
|
end
|
|
139
|
+
rescue StandardError => e
|
|
140
|
+
Logging::KameleoonLogger.error('Error occurred within tracking request: %s', e)
|
|
141
|
+
keep_for_later(visitor_codes, visitor_data)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def keep_for_later(visitor_codes, visitor_data)
|
|
145
|
+
visitor_data.each { |d| d.mark_as_unsent if d.is_a?(Kameleoon::Data) }
|
|
146
|
+
@tracking_visitors.add_all(visitor_codes)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def enter_probe_mode
|
|
150
|
+
return if @probe_mode
|
|
151
|
+
|
|
152
|
+
@probe_mode = true
|
|
153
|
+
Logging::KameleoonLogger.info("Tracking request failed: only one visitor's data per tracking request " \
|
|
154
|
+
'will be sent until a request succeeds')
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def leave_probe_mode
|
|
158
|
+
return unless @probe_mode
|
|
159
|
+
|
|
160
|
+
@probe_mode = false
|
|
161
|
+
Logging::KameleoonLogger.info('Tracking request succeeded: full-size tracking requests are restored')
|
|
94
162
|
end
|
|
95
163
|
end
|
|
96
164
|
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
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'net/http'
|
|
4
|
+
require 'net/http/persistent'
|
|
4
5
|
require 'kameleoon/version'
|
|
5
6
|
require 'kameleoon/network/response'
|
|
7
|
+
require 'kameleoon/logging/kameleoon_logger'
|
|
6
8
|
require 'kameleoon/exceptions'
|
|
7
9
|
|
|
8
10
|
module Kameleoon
|
|
@@ -12,6 +14,9 @@ module Kameleoon
|
|
|
12
14
|
raise KameleoonError, 'Call of not implemented method!'
|
|
13
15
|
end
|
|
14
16
|
|
|
17
|
+
# Releases the resources held by the provider (e.g. kept-alive connections); no-op by default.
|
|
18
|
+
def close; end
|
|
19
|
+
|
|
15
20
|
private
|
|
16
21
|
|
|
17
22
|
def collect_headers(request)
|
|
@@ -29,7 +34,27 @@ module Kameleoon
|
|
|
29
34
|
end
|
|
30
35
|
end
|
|
31
36
|
|
|
37
|
+
# Performs requests over kept-alive connections managed by `Net::HTTP::Persistent`.
|
|
38
|
+
#
|
|
39
|
+
# `Net::HTTP::Persistent` timeouts are per client, while the SDK timeout is per request, so one client (with its
|
|
40
|
+
# own connection pool) is kept per distinct timeout value. In practice that is the default timeout plus the few
|
|
41
|
+
# values callers pass explicitly.
|
|
32
42
|
class SyncNetProvider < NetProvider
|
|
43
|
+
# Idle connections are reconnected before use after this long; keeps a request from hitting a connection the
|
|
44
|
+
# server has already dropped.
|
|
45
|
+
IDLE_TIMEOUT = 15 # seconds
|
|
46
|
+
# Guards against a caller passing a different timeout on every request (each value would keep its own idle
|
|
47
|
+
# connections). Requests with further timeout values are sent on a one-off connection with their own timeout.
|
|
48
|
+
MAX_CLIENTS = 16
|
|
49
|
+
|
|
50
|
+
def initialize
|
|
51
|
+
super()
|
|
52
|
+
@clients = {}
|
|
53
|
+
@clients_mutex = Mutex.new
|
|
54
|
+
@overflow_warned = false
|
|
55
|
+
@closed = false
|
|
56
|
+
end
|
|
57
|
+
|
|
33
58
|
def make_request(request)
|
|
34
59
|
for _ in 0..8 do
|
|
35
60
|
response = make_single_request(request)
|
|
@@ -44,10 +69,17 @@ module Kameleoon
|
|
|
44
69
|
response
|
|
45
70
|
end
|
|
46
71
|
|
|
72
|
+
def close
|
|
73
|
+
@clients_mutex.synchronize do
|
|
74
|
+
@closed = true
|
|
75
|
+
@clients.each_value(&:shutdown)
|
|
76
|
+
@clients.clear
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
47
80
|
private
|
|
48
81
|
|
|
49
82
|
def make_single_request(request)
|
|
50
|
-
resp = nil
|
|
51
83
|
case request.method
|
|
52
84
|
when Method::GET
|
|
53
85
|
req = Net::HTTP::Get.new(request.url)
|
|
@@ -57,21 +89,71 @@ module Kameleoon
|
|
|
57
89
|
else
|
|
58
90
|
return unknown_method_response(request.method, request)
|
|
59
91
|
end
|
|
60
|
-
|
|
61
|
-
headers = collect_headers(request)
|
|
62
|
-
headers.each { |k, v| req[k] = v }
|
|
92
|
+
collect_headers(request).each { |k, v| req[k] = v }
|
|
63
93
|
uri = URI(request.url)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
end
|
|
94
|
+
timeout = request.timeout.to_f / 1000.0
|
|
95
|
+
client = client_for(timeout)
|
|
96
|
+
resp = client ? client.request(uri, req) : request_once(uri, req, timeout)
|
|
68
97
|
body = resp.body
|
|
69
98
|
body = nil if body&.empty?
|
|
70
|
-
|
|
71
|
-
Response.new(nil, resp.code.to_i, body, headers, request)
|
|
99
|
+
Response.new(nil, resp.code.to_i, body, resp.to_hash, request)
|
|
72
100
|
rescue StandardError => e
|
|
73
101
|
Response.new(e, nil, nil, nil, request)
|
|
74
102
|
end
|
|
103
|
+
|
|
104
|
+
def client_for(timeout)
|
|
105
|
+
@clients_mutex.synchronize do
|
|
106
|
+
return nil if @closed
|
|
107
|
+
|
|
108
|
+
client = @clients[timeout]
|
|
109
|
+
return client if client
|
|
110
|
+
|
|
111
|
+
if @clients.size >= MAX_CLIENTS
|
|
112
|
+
unless @overflow_warned
|
|
113
|
+
@overflow_warned = true
|
|
114
|
+
Logging::KameleoonLogger.warning(
|
|
115
|
+
'More than %s distinct request timeouts in use; requests with further timeout values ' \
|
|
116
|
+
'are sent without connection reuse', MAX_CLIENTS
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
return nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
@clients[timeout] = build_client(timeout)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Sends the request on a connection which is closed afterwards, honouring `timeout` (seconds).
|
|
127
|
+
def request_once(uri, req, timeout)
|
|
128
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https', open_timeout: timeout,
|
|
129
|
+
read_timeout: timeout, write_timeout: timeout) do |http|
|
|
130
|
+
http.request(req)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The timeout is applied as given, also when it is zero or negative (as `Net::HTTP` did before pooling):
|
|
135
|
+
# validating it is the callers' concern.
|
|
136
|
+
def build_client(timeout)
|
|
137
|
+
http = Net::HTTP::Persistent.new(name: "kameleoon-sdk-#{timeout}", proxy: proxy_setting)
|
|
138
|
+
http.idle_timeout = IDLE_TIMEOUT
|
|
139
|
+
http.open_timeout = timeout
|
|
140
|
+
http.read_timeout = timeout
|
|
141
|
+
http.write_timeout = timeout
|
|
142
|
+
http
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# `:ENV` keeps the `http_proxy`/`no_proxy` support that plain `Net::HTTP` has by default. net-http-persistent
|
|
146
|
+
# 4.0.8 parses `no_proxy` with `Array#filter_map` (Ruby 2.7+), so on older Rubies with `no_proxy` set the proxy
|
|
147
|
+
# URI is passed directly, without the bypass list (Kameleoon hosts are not expected to be in it anyway).
|
|
148
|
+
def proxy_setting
|
|
149
|
+
no_proxy = ENV['no_proxy'] || ENV['NO_PROXY']
|
|
150
|
+
return :ENV if RUBY_VERSION >= '2.7' || no_proxy.nil? || no_proxy.empty?
|
|
151
|
+
|
|
152
|
+
env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
|
|
153
|
+
return nil if env_proxy.nil? || env_proxy.empty? || no_proxy == '*'
|
|
154
|
+
|
|
155
|
+
URI(env_proxy.match?(%r{\Ahttps?://}) ? env_proxy : "http://#{env_proxy}")
|
|
156
|
+
end
|
|
75
157
|
end
|
|
76
158
|
end
|
|
77
159
|
end
|
|
@@ -86,6 +86,12 @@ module Kameleoon
|
|
|
86
86
|
unwrap_response(*make_call(Events::RequestType::ACCESS_TOKEN, request, false))
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
# Closes the kept-alive connections. A request still running completes on its connection; a request started
|
|
90
|
+
# afterwards fails with an error response.
|
|
91
|
+
def close
|
|
92
|
+
@sync_net_provider.close
|
|
93
|
+
end
|
|
94
|
+
|
|
89
95
|
private
|
|
90
96
|
|
|
91
97
|
def make_call(request_type, request, try_access_token_auth, retry_limit = 0, retry_delay = 0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'net/http'
|
|
4
4
|
|
|
5
5
|
module Kameleoon
|
|
6
6
|
module RealTime
|
|
@@ -31,8 +31,11 @@ module Kameleoon
|
|
|
31
31
|
##
|
|
32
32
|
# Starts SSE connection and stay in the loop until close.
|
|
33
33
|
def start
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
uri = URI(@url)
|
|
35
|
+
# No read timeout: the stream is expected to stay silent between events.
|
|
36
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', read_timeout: nil) do |http|
|
|
37
|
+
http.request(Net::HTTP::Get.new(uri, @headers)) { |resp| handle_resp(resp) }
|
|
38
|
+
end
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
##
|
data/lib/kameleoon/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,43 +1,29 @@
|
|
|
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.2
|
|
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-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: concurrent-ruby
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: 1.1.5
|
|
20
|
-
type: :runtime
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - ">="
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: 1.1.5
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: em-synchrony
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - ">="
|
|
17
|
+
- - "~>"
|
|
32
18
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 1.
|
|
19
|
+
version: '1.2'
|
|
34
20
|
type: :runtime
|
|
35
21
|
prerelease: false
|
|
36
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
23
|
requirements:
|
|
38
|
-
- - "
|
|
24
|
+
- - "~>"
|
|
39
25
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 1.
|
|
26
|
+
version: '1.2'
|
|
41
27
|
- !ruby/object:Gem::Dependency
|
|
42
28
|
name: rufus-scheduler
|
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -53,19 +39,19 @@ dependencies:
|
|
|
53
39
|
- !ruby/object:Gem::Version
|
|
54
40
|
version: '3.7'
|
|
55
41
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
42
|
+
name: net-http-persistent
|
|
57
43
|
requirement: !ruby/object:Gem::Requirement
|
|
58
44
|
requirements:
|
|
59
45
|
- - "~>"
|
|
60
46
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
47
|
+
version: '4.0'
|
|
62
48
|
type: :runtime
|
|
63
49
|
prerelease: false
|
|
64
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
51
|
requirements:
|
|
66
52
|
- - "~>"
|
|
67
53
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
54
|
+
version: '4.0'
|
|
69
55
|
description: Kameleoon Client Ruby Software Development Kit
|
|
70
56
|
email:
|
|
71
57
|
- sdk@kameleoon.com
|
|
@@ -155,8 +141,6 @@ files:
|
|
|
155
141
|
- lib/kameleoon/real_time/sse_message.rb
|
|
156
142
|
- lib/kameleoon/real_time/sse_request.rb
|
|
157
143
|
- lib/kameleoon/sem_version.rb
|
|
158
|
-
- lib/kameleoon/storage/cache.rb
|
|
159
|
-
- lib/kameleoon/storage/cache_factory.rb
|
|
160
144
|
- lib/kameleoon/targeting/condition.rb
|
|
161
145
|
- lib/kameleoon/targeting/condition_factory.rb
|
|
162
146
|
- 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
|