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.
@@ -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,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 = 3
18
- TRACKING_CALL_ATTEMPT_NUMBER = 3
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, FETCH_CONFIGURATION_ATTEMPT_NUMBER - 1)
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, TRACKING_CALL_ATTEMPT_NUMBER - 1, TRACKING_CALL_RETRY_DELAY))
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Kameleoon Real Time Configuration Service
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
- CONFIGURATION_UPDATE_EVENT = 'configuration-update-event'
13
+ DATA_FILE_UPDATE_EVENT = 'configuration-update-event'
14
14
 
15
15
  ##
16
- # RealTimeConfigurationService is used for fetching updates of configuration
16
+ # RealTimeEventService is used for fetching updates of configuration
17
17
  # (experiments and feature flags) in real time.
18
- class RealTimeConfigurationService
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
- def initialize(url, update_handler, sse_request_source = nil)
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 configuration service is shutting down')
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 == CONFIGURATION_UPDATE_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 (true)
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 true', type)
12
- true
11
+ Logging::KameleoonLogger.warning('Condition of unknown type \'%s\' evaluated as false', type)
12
+ false
13
13
  end
14
14
  end
15
15
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kameleoon
4
- SDK_VERSION = '3.21.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.21.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-07-02 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
@@ -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