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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f492badef67f0b4e09561d3a3d423e4497dbd7efe22d07641cac9485fe9e100
4
- data.tar.gz: 95f7268b78ecd64bd4707c8fc8581251b8f07038a32eb044aebdb0b1cab652b6
3
+ metadata.gz: 04efd3ab470f352395de3161c329086e07893f9a29d2e8d55085cc4ab00788c3
4
+ data.tar.gz: 62d498e4f19ec285358b29672d622e0476e70ae31cbf39bd480bf33dbe5b335b
5
5
  SHA512:
6
- metadata.gz: 194b5aeee439890f068abe5b0b4c0f11fb508844de81e54e5b5582cefed5c7849d17054cb469994b57244177b61b9cfb89ccfecf771d9c135cc7126a277140e8
7
- data.tar.gz: 114ecb570390aa32386abf7074463d903884d6faadf8c7b3de77953bc38152dc70cdf3eb7d5d8ac64de127014f318a2d2e53359c830cb9af5d8f3118d581b8d7
6
+ metadata.gz: 0431f162cc71fb10db0c3920bef2a9d04c35713dfdfd467fc750ac16345c3cfd5965b8abb17c335ad8a7e7226d85c776a14d5d76030c7173969b00a61b309fd1
7
+ data.tar.gz: 683b9a9e65838df67344bc3b5ff05d8b04eb37fe5328ec4532f4d18b141d9957ca6709b3373adc22ce4f52791d3d0e90be8927693acfe159add2ba991812dc16
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
3
4
  require 'kameleoon/logging/kameleoon_logger'
4
5
 
5
6
  module Kameleoon
@@ -17,7 +17,7 @@ module Kameleoon
17
17
  end
18
18
  end
19
19
 
20
- # Implementation of Cache with auto cleaning feature
20
+ # Implementation of Manager with auto-expiring visitor variations
21
21
  class ManagerImpl < Manager
22
22
  def initialize(expiration_time, data_manager)
23
23
  super()
@@ -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 = LockVisitorTrackingRegistry.new(visitor_manager)
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
- track(@tracking_visitors.extract)
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
- class LockVisitorTrackingRegistry
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 = Set.new
17
- @mutex = Mutex.new
26
+ @visitors = {}
27
+ @extract_mutex = Mutex.new
18
28
  end
19
29
 
20
- def add(visitor_code)
21
- @mutex.synchronize { @visitors.add(visitor_code) }
22
- end
30
+ if LOCK_FREE_ADD
31
+ def add(visitor_code)
32
+ @visitors[visitor_code] = true
33
+ end
23
34
 
24
- def add_all(visitor_codes)
25
- @mutex.synchronize do
26
- @visitors.merge(visitor_codes)
27
- if @visitors.size > @storage_limit
28
- erase_nonexistent_visitors
29
- erase_to_storage_limit
30
- end
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
- def extract
35
- should_extract_all_be_used ? extract_all : extract_limited
56
+ # Test purpose only
57
+ def visitors
58
+ @visitors.keys
36
59
  end
37
60
 
38
61
  private
39
62
 
40
- def should_extract_all_be_used
41
- @visitors.size < @extraction_limit * LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT
42
- end
63
+ def extraction_count(size, limit)
64
+ return size if (size <= limit) && (size < @extraction_limit * LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT)
43
65
 
44
- def extract_all
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
- def extract_limited
55
- extracted = nil
56
- @mutex.synchronize do
57
- if should_extract_all_be_used
58
- extracted = extract_all
59
- next
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
- extracted.push(vc)
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
- visitors_to_remove_count = @visitors.size - (@storage_limit * REMOVAL_FACTOR).to_i
82
- return if visitors_to_remove_count <= 0
83
-
84
- @visitors.each do |vc|
85
- break if visitors_to_remove_count.zero?
86
-
87
- visitors_to_remove_count -= 1
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
- @fetching = false
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.getToken(timeout: %s)', timeout)
42
- now = Time.new.to_f
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
- return call_fetch_token(timeout) if token.nil? || token.expired?(now)
45
-
46
- run_fetch_token if !@fetching && token.obsolete?(now)
47
- Logging::KameleoonLogger.debug("RETURN: AccessTokenSource.getToken(timeout: %s) -> (token: '%s')",
48
- timeout, token.value)
49
- token.value
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
- def call_fetch_token(timeout)
62
- @fetching = true
63
- fetch_token(timeout)
64
- rescue StandardError => e
65
- @fetching = false
66
- Logging::KameleoonLogger.error("Failed to call access token fetching: #{e}")
67
- nil
68
- end
69
-
70
- def run_fetch_token
71
- # setting `@fetching` here to reduce the number of requests until new thread started
72
- @fetching = true
73
- Thread.new { fetch_token }
74
- rescue StandardError => e
75
- @fetching = false
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 = jwt[JWT_ACCESS_TOKEN_FIELD]
89
- expires_in = jwt[JWT_EXPIRES_IN_FIELD]
90
- rescue JSON::ParserError => e
91
- Logging::KameleoonLogger.error("Failed to parse access JWT: #{e}")
92
- return nil
93
- end
94
- unless token.is_a?(String) && !token.empty? && expires_in.is_a?(Integer) && expires_in.positive?
95
- Logging::KameleoonLogger.error('Failed to read access JWT')
96
- return nil
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
- token = handle_fetched_token(token, expires_in)
99
- Logging::KameleoonLogger.debug(
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 handle_fetched_token(token, expires_in)
108
- now = Time.new.to_f
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
- @cached_token = ExpiringToken.new(token, exp_time, obs_time)
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,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kameleoon
4
- SDK_VERSION = '3.22.0'
4
+ SDK_VERSION = '3.22.1'
5
5
  SDK_NAME = 'RUBY'
6
6
  end
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.0
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-08-27 00:00:00.000000000 Z
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