configcat 5.0.2 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,153 @@
1
+ require 'configcat/datagovernance'
2
+ require 'configcat/pollingmode'
3
+
4
+ module ConfigCat
5
+ class Hooks
6
+ #
7
+ # Events fired by [ConfigCatClient].
8
+ #
9
+
10
+ def initialize(on_client_ready: nil, on_config_changed: nil, on_flag_evaluated: nil, on_error: nil)
11
+ @_on_client_ready_callbacks = on_client_ready ? [on_client_ready] : []
12
+ @_on_config_changed_callbacks = on_config_changed ? [on_config_changed] : []
13
+ @_on_flag_evaluated_callbacks = on_flag_evaluated ? [on_flag_evaluated] : []
14
+ @_on_error_callbacks = on_error ? [on_error] : []
15
+ end
16
+
17
+ def add_on_client_ready(callback)
18
+ @_on_client_ready_callbacks.push(callback)
19
+ end
20
+
21
+ def add_on_config_changed(callback)
22
+ @_on_config_changed_callbacks.push(callback)
23
+ end
24
+
25
+ def add_on_flag_evaluated(callback)
26
+ @_on_flag_evaluated_callbacks.push(callback)
27
+ end
28
+
29
+ def add_on_error(callback)
30
+ @_on_error_callbacks.push(callback)
31
+ end
32
+
33
+ def invoke_on_client_ready
34
+ @_on_client_ready_callbacks.each { |callback|
35
+ begin
36
+ callback.()
37
+ rescue Exception => e
38
+ error = "Exception occurred during invoke_on_client_ready callback: #{e}"
39
+ invoke_on_error(error)
40
+ ConfigCat.logger.error(error)
41
+ end
42
+ }
43
+ end
44
+
45
+ def invoke_on_config_changed(config)
46
+ @_on_config_changed_callbacks.each { |callback|
47
+ begin
48
+ callback.(config)
49
+ rescue Exception => e
50
+ error = "Exception occurred during invoke_on_config_changed callback: #{e}"
51
+ invoke_on_error(error)
52
+ ConfigCat.logger.error(error)
53
+ end
54
+ }
55
+ end
56
+
57
+ def invoke_on_flag_evaluated(evaluation_details)
58
+ @_on_flag_evaluated_callbacks.each { |callback|
59
+ begin
60
+ callback.(evaluation_details)
61
+ rescue Exception => e
62
+ error = "Exception occurred during invoke_on_flag_evaluated callback: #{e}"
63
+ invoke_on_error(error)
64
+ ConfigCat.logger.error(error)
65
+ end
66
+ }
67
+ end
68
+
69
+ def invoke_on_error(error)
70
+ @_on_error_callbacks.each { |callback|
71
+ begin
72
+ callback.(error)
73
+ rescue Exception => e
74
+ ConfigCat.logger.error("Exception occurred during invoke_on_error callback: #{e}")
75
+ end
76
+ }
77
+ end
78
+
79
+ def clear
80
+ @_on_client_ready_callbacks.clear
81
+ @_on_config_changed_callbacks.clear
82
+ @_on_flag_evaluated_callbacks.clear
83
+ @_on_error_callbacks.clear
84
+ end
85
+ end
86
+
87
+ class ConfigCatOptions
88
+ # Configuration options for ConfigCatClient.
89
+ attr_reader :base_url, :polling_mode, :config_cache, :proxy_address, :proxy_port, :proxy_user, :proxy_pass,
90
+ :open_timeout_seconds, :read_timeout_seconds, :flag_overrides, :data_governance, :default_user,
91
+ :hooks, :offline
92
+
93
+ def initialize(base_url: nil,
94
+ polling_mode: PollingMode.auto_poll(),
95
+ config_cache: nil,
96
+ proxy_address: nil,
97
+ proxy_port: nil,
98
+ proxy_user: nil,
99
+ proxy_pass: nil,
100
+ open_timeout_seconds: 10,
101
+ read_timeout_seconds: 30,
102
+ flag_overrides: nil,
103
+ data_governance: DataGovernance::GLOBAL,
104
+ default_user: nil,
105
+ hooks: nil,
106
+ offline: false)
107
+ # The base ConfigCat CDN url.
108
+ @base_url = base_url
109
+
110
+ # The polling mode.
111
+ @polling_mode = polling_mode
112
+
113
+ # The cache implementation used to cache the downloaded config files.
114
+ @config_cache = config_cache
115
+
116
+ # Proxy address
117
+ @proxy_address = proxy_address
118
+
119
+ # Proxy port
120
+ @proxy_port = proxy_port
121
+
122
+ # username for proxy authentication
123
+ @proxy_user = proxy_user
124
+
125
+ # password for proxy authentication
126
+ @proxy_pass = proxy_pass
127
+
128
+ # The number of seconds to wait for the server to make the initial connection
129
+ # (i.e. completing the TCP connection handshake).
130
+ @open_timeout_seconds = open_timeout_seconds
131
+
132
+ # The number of seconds to wait for the server to respond before giving up.
133
+ @read_timeout_seconds = read_timeout_seconds
134
+
135
+ # Feature flag and setting overrides.
136
+ @flag_overrides = flag_overrides
137
+
138
+ # Default: `DataGovernance.Global`. Set this parameter to be in sync with the
139
+ # Data Governance preference on the [Dashboard](https://app.configcat.com/organization/data-governance).
140
+ # (Only Organization Admins have access)
141
+ @data_governance = data_governance
142
+
143
+ # The default user to be used for evaluating feature flags and getting settings.
144
+ @default_user = default_user
145
+
146
+ # The Hooks instance to subscribe to events.
147
+ @hooks = hooks
148
+
149
+ # Indicates whether the client should work in offline mode.
150
+ @offline = offline
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,40 @@
1
+ require 'configcat/utils'
2
+
3
+ module ConfigCat
4
+ class ConfigEntry
5
+ CONFIG = 'config'
6
+ ETAG = 'etag'
7
+ FETCH_TIME = 'fetch_time'
8
+
9
+ attr_accessor :config, :etag, :fetch_time
10
+
11
+ def initialize(config = {}, etag = '', fetch_time = Utils::DISTANT_PAST)
12
+ @config = config
13
+ @etag = etag
14
+ @fetch_time = fetch_time
15
+ end
16
+
17
+ def self.create_from_json(json)
18
+ return ConfigEntry::EMPTY if json.nil?
19
+ return ConfigEntry.new(
20
+ config = json.fetch(CONFIG, {}),
21
+ etag = json.fetch(ETAG, ''),
22
+ fetch_time = json.fetch(FETCH_TIME, Utils::DISTANT_PAST)
23
+ )
24
+ end
25
+
26
+ def empty?
27
+ self == ConfigEntry::EMPTY
28
+ end
29
+
30
+ def to_json
31
+ {
32
+ CONFIG => config,
33
+ ETAG => etag,
34
+ FETCH_TIME => fetch_time
35
+ }
36
+ end
37
+
38
+ EMPTY = ConfigEntry.new(etag: 'empty')
39
+ end
40
+ end
@@ -2,6 +2,7 @@ require 'configcat/interfaces'
2
2
  require 'configcat/version'
3
3
  require 'configcat/datagovernance'
4
4
  require 'configcat/constants'
5
+ require 'configcat/configentry'
5
6
  require 'net/http'
6
7
  require 'uri'
7
8
  require 'json'
@@ -18,41 +19,66 @@ module ConfigCat
18
19
  FORCE_REDIRECT = 2
19
20
  end
20
21
 
22
+ class Status
23
+ FETCHED = 0,
24
+ NOT_MODIFIED = 1,
25
+ FAILURE = 2
26
+ end
27
+
21
28
  class FetchResponse
22
- def initialize(response)
23
- @_response = response
29
+ attr_reader :entry, :error, :is_transient_error
30
+
31
+ def initialize(status, entry, error = nil, is_transient_error = false)
32
+ @status = status
33
+ @entry = entry
34
+ @error = error
35
+ @is_transient_error = is_transient_error
36
+ end
37
+
38
+ # Gets whether a new configuration value was fetched or not.
39
+ # :return [Boolean] true if a new configuration value was fetched, otherwise false.
40
+ def is_fetched
41
+ @status == Status::FETCHED
42
+ end
43
+
44
+ # Gets whether the fetch resulted a '304 Not Modified' or not.
45
+ # :return [Boolean] true if the fetch resulted a '304 Not Modified' code, otherwise false.
46
+ def is_not_modified
47
+ @status == Status::NOT_MODIFIED
24
48
  end
25
49
 
26
- # Returns the json-encoded content of a response, if any
27
- def json()
28
- return JSON.parse(@_response.body)
50
+ # Gets whether the fetch failed or not.
51
+ # :return [Boolean] true if the fetch failed, otherwise false.
52
+ def is_failed
53
+ @status == Status::FAILURE
29
54
  end
30
55
 
31
- # Gets whether a new configuration value was fetched or not
32
- def is_fetched()
33
- code = @_response.code.to_i
34
- return 200 <= code && code < 300
56
+ def self.success(entry)
57
+ FetchResponse.new(Status::FETCHED, entry)
35
58
  end
36
59
 
37
- # Gets whether the fetch resulted a '304 Not Modified' or not
38
- def is_not_modified()
39
- return @_response.code == "304"
60
+ def self.not_modified
61
+ FetchResponse.new(Status::NOT_MODIFIED, ConfigEntry::EMPTY)
62
+ end
63
+
64
+ def self.failure(error, is_transient_error)
65
+ FetchResponse.new(Status::FAILURE, ConfigEntry::EMPTY, error, is_transient_error)
40
66
  end
41
67
  end
42
68
 
43
- class CacheControlConfigFetcher < ConfigFetcher
44
- def initialize(sdk_key, mode, base_url:nil, proxy_address:nil, proxy_port:nil, proxy_user:nil, proxy_pass:nil,
45
- open_timeout:10, read_timeout:30,
46
- data_governance:DataGovernance::GLOBAL)
69
+ class ConfigFetcher
70
+ def initialize(sdk_key, log, mode, base_url: nil, proxy_address: nil, proxy_port: nil, proxy_user: nil, proxy_pass: nil,
71
+ open_timeout: 10, read_timeout: 30,
72
+ data_governance: DataGovernance::GLOBAL)
47
73
  @_sdk_key = sdk_key
74
+ @log = log
48
75
  @_proxy_address = proxy_address
49
76
  @_proxy_port = proxy_port
50
77
  @_proxy_user = proxy_user
51
78
  @_proxy_pass = proxy_pass
52
79
  @_open_timeout = open_timeout
53
80
  @_read_timeout = read_timeout
54
- @_etag = ""
55
- @_headers = {"User-Agent" => ((("ConfigCat-Ruby/") + mode) + ("-")) + VERSION, "X-ConfigCat-UserAgent" => ((("ConfigCat-Ruby/") + mode) + ("-")) + VERSION, "Content-Type" => "application/json"}
81
+ @_headers = { "User-Agent" => ((("ConfigCat-Ruby/") + mode) + ("-")) + VERSION, "X-ConfigCat-UserAgent" => ((("ConfigCat-Ruby/") + mode) + ("-")) + VERSION, "Content-Type" => "application/json" }
56
82
  if !base_url.equal?(nil)
57
83
  @_base_url_overridden = true
58
84
  @_base_url = base_url.chomp("/")
@@ -66,34 +92,24 @@ module ConfigCat
66
92
  end
67
93
  end
68
94
 
69
- def get_open_timeout()
95
+ def get_open_timeout
70
96
  return @_open_timeout
71
97
  end
72
98
 
73
- def get_read_timeout()
99
+ def get_read_timeout
74
100
  return @_read_timeout
75
101
  end
76
102
 
77
- # Returns the FetchResponse object contains configuration json Dictionary
78
- def get_configuration_json(retries=0)
79
- ConfigCat.logger.debug "Fetching configuration from ConfigCat"
80
- uri = URI.parse((((@_base_url + ("/")) + BASE_PATH) + @_sdk_key) + BASE_EXTENSION)
81
- headers = @_headers
82
- headers["If-None-Match"] = @_etag unless @_etag.empty?
83
- _create_http()
84
- request = Net::HTTP::Get.new(uri.request_uri, headers)
85
- response = @_http.request(request)
86
- etag = response["ETag"]
87
- @_etag = etag unless etag.nil? || etag.empty?
88
- ConfigCat.logger.debug "ConfigCat configuration json fetch response code:#{response.code} Cached:#{response['ETag']}"
89
- fetch_response = FetchResponse.new(response)
103
+ # Returns the FetchResponse object contains configuration entry
104
+ def get_configuration(etag = "", retries = 0)
105
+ fetch_response = _fetch(etag)
90
106
 
91
107
  # If there wasn't a config change, we return the response.
92
108
  if !fetch_response.is_fetched()
93
109
  return fetch_response
94
110
  end
95
111
 
96
- preferences = fetch_response.json().fetch(PREFERENCES, nil)
112
+ preferences = fetch_response.entry.config.fetch(PREFERENCES, nil)
97
113
  if preferences === nil
98
114
  return fetch_response
99
115
  end
@@ -107,7 +123,7 @@ module ConfigCat
107
123
 
108
124
  redirect = preferences.fetch(REDIRECT, nil)
109
125
  # If the base_url is overridden, and the redirect parameter is not 2 (force),
110
- # the SDK should not redirect the calls and it just have to return the response.
126
+ # the SDK should not redirect the calls, and it just has to return the response.
111
127
  if @_base_url_overridden && redirect != RedirectMode::FORCE_REDIRECT
112
128
  return fetch_response
113
129
  end
@@ -124,20 +140,21 @@ module ConfigCat
124
140
  # Try to download again with the new url
125
141
 
126
142
  if redirect == RedirectMode::SHOULD_REDIRECT
127
- ConfigCat.logger.warn("Your data_governance parameter at ConfigCatClient initialization is not in sync with your preferences on the ConfigCat Dashboard: https://app.configcat.com/organization/data-governance. Only Organization Admins can set this preference.")
143
+ @log.warn(3002, "The `dataGovernance` parameter specified at the client initialization is not in sync with the preferences on the ConfigCat Dashboard. " \
144
+ "Read more: https://configcat.com/docs/advanced/data-governance/")
128
145
  end
129
146
 
130
147
  # To prevent loops we check if we retried at least 3 times with the new base_url
131
148
  if retries >= 2
132
- ConfigCat.logger.error("Redirect loop during config.json fetch. Please contact support@configcat.com.")
149
+ @log.error(1104, "Redirection loop encountered while trying to fetch config JSON. Please contact us at https://configcat.com/support/")
133
150
  return fetch_response
134
151
  end
135
152
 
136
153
  # Retry the config download with the new base_url
137
- return get_configuration_json(retries + 1)
154
+ return get_configuration(etag, retries + 1)
138
155
  end
139
156
 
140
- def close()
157
+ def close
141
158
  if @_http
142
159
  @_http = nil
143
160
  end
@@ -145,7 +162,49 @@ module ConfigCat
145
162
 
146
163
  private
147
164
 
148
- def _create_http()
165
+ def _fetch(etag)
166
+ begin
167
+ @log.debug("Fetching configuration from ConfigCat")
168
+ uri = URI.parse((((@_base_url + ("/")) + BASE_PATH) + @_sdk_key) + BASE_EXTENSION)
169
+ headers = @_headers
170
+ headers["If-None-Match"] = etag.empty? ? nil : etag
171
+ _create_http()
172
+ request = Net::HTTP::Get.new(uri.request_uri, headers)
173
+ response = @_http.request(request)
174
+ case response
175
+ when Net::HTTPSuccess
176
+ @log.debug("ConfigCat configuration json fetch response code:#{response.code} Cached:#{response['ETag']}")
177
+ response_etag = response["ETag"]
178
+ if response_etag.nil?
179
+ response_etag = ""
180
+ end
181
+ config = JSON.parse(response.body)
182
+ return FetchResponse.success(ConfigEntry.new(config, response_etag, Utils.get_utc_now_seconds_since_epoch))
183
+ when Net::HTTPNotModified
184
+ return FetchResponse.not_modified
185
+ when Net::HTTPNotFound, Net::HTTPForbidden
186
+ error = "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received unexpected response: #{response}"
187
+ @log.error(1100, error)
188
+ return FetchResponse.failure(error, false)
189
+ else
190
+ raise Net::HTTPError.new("", response)
191
+ end
192
+ rescue Net::HTTPError => e
193
+ error = "Unexpected HTTP response was received while trying to fetch config JSON: #{e}"
194
+ @log.error(1101, error)
195
+ return FetchResponse.failure(error, true)
196
+ rescue Timeout::Error => e
197
+ error = "Request timed out while trying to fetch config JSON. Timeout values: [connect: #{get_open_timeout()}s, read: #{get_read_timeout()}s]"
198
+ @log.error(1102, error)
199
+ return FetchResponse.failure(error, true)
200
+ rescue Exception => e
201
+ error = "Unexpected error occurred while trying to fetch config JSON: #{e}"
202
+ @log.error(1103, error)
203
+ return FetchResponse.failure(error, true)
204
+ end
205
+ end
206
+
207
+ def _create_http
149
208
  uri = URI.parse(@_base_url)
150
209
  use_ssl = true if uri.scheme == 'https'
151
210
  if @_http.equal?(nil) || @_http.address != uri.host || @_http.port != uri.port || @_http.use_ssl? != use_ssl
@@ -0,0 +1,219 @@
1
+ require 'concurrent'
2
+ require 'configcat/configentry'
3
+ require 'configcat/pollingmode'
4
+ require 'configcat/refreshresult'
5
+
6
+
7
+ module ConfigCat
8
+ class ConfigService
9
+ def initialize(sdk_key, polling_mode, hooks, config_fetcher, log, config_cache, is_offline)
10
+ @sdk_key = sdk_key
11
+ @cached_entry = ConfigEntry::EMPTY
12
+ @cached_entry_string = ''
13
+ @polling_mode = polling_mode
14
+ @log = log
15
+ @config_cache = config_cache
16
+ @hooks = hooks
17
+ @cache_key = Digest::SHA1.hexdigest("ruby_#{CONFIG_FILE_NAME}_#{@sdk_key}")
18
+ @config_fetcher = config_fetcher
19
+ @is_offline = is_offline
20
+ @response_future = nil
21
+ @initialized = Concurrent::Event.new
22
+ @lock = Mutex.new
23
+ @ongoing_fetch = false
24
+ @fetch_finished = Concurrent::Event.new
25
+ @start_time = Utils.get_utc_now_seconds_since_epoch
26
+
27
+ if @polling_mode.is_a?(AutoPollingMode) && !@is_offline
28
+ start_poll
29
+ else
30
+ set_initialized
31
+ end
32
+ end
33
+
34
+ def get_settings
35
+ if @polling_mode.is_a?(LazyLoadingMode)
36
+ entry, _ = fetch_if_older(Utils.get_utc_now_seconds_since_epoch - @polling_mode.cache_refresh_interval_seconds)
37
+ return !entry.empty? ?
38
+ [entry.config.fetch(FEATURE_FLAGS, {}), entry.fetch_time] :
39
+ [nil, Utils::DISTANT_PAST]
40
+
41
+ elsif @polling_mode.is_a?(AutoPollingMode) && !@initialized.set?
42
+ elapsed_time = Utils.get_utc_now_seconds_since_epoch - @start_time # Elapsed time in seconds
43
+ if elapsed_time < @polling_mode.max_init_wait_time_seconds
44
+ @initialized.wait(@polling_mode.max_init_wait_time_seconds - elapsed_time)
45
+
46
+ # Max wait time expired without result, notify subscribers with the cached config.
47
+ if !@initialized.set?
48
+ set_initialized
49
+ return !@cached_entry.empty? ?
50
+ [@cached_entry.config.fetch(FEATURE_FLAGS, {}), @cached_entry.fetch_time] :
51
+ [nil, Utils::DISTANT_PAST]
52
+ end
53
+ end
54
+ end
55
+
56
+ entry, _ = fetch_if_older(Utils::DISTANT_PAST, prefer_cache: true)
57
+ return !entry.empty? ?
58
+ [entry.config.fetch(FEATURE_FLAGS, {}), entry.fetch_time] :
59
+ [nil, Utils::DISTANT_PAST]
60
+ end
61
+
62
+ # :return [RefreshResult]
63
+ def refresh
64
+ _, error = fetch_if_older(Utils::DISTANT_FUTURE)
65
+ return RefreshResult.new(success = error.nil?, error = error)
66
+ end
67
+
68
+ def set_online
69
+ @lock.synchronize do
70
+ if !@is_offline
71
+ return
72
+ end
73
+
74
+ @is_offline = false
75
+ if @polling_mode.is_a?(AutoPollingMode)
76
+ start_poll
77
+ end
78
+ @log.info(5200, 'Switched to ONLINE mode.')
79
+ end
80
+ end
81
+
82
+ def set_offline
83
+ @lock.synchronize do
84
+ if @is_offline
85
+ return
86
+ end
87
+
88
+ @is_offline = true
89
+ if @polling_mode.is_a?(AutoPollingMode)
90
+ @stopped.set
91
+ @thread.join
92
+ end
93
+
94
+ @log.info(5200, 'Switched to OFFLINE mode.')
95
+ end
96
+ end
97
+
98
+ def offline?
99
+ return @is_offline
100
+ end
101
+
102
+ def close
103
+ if @polling_mode.is_a?(AutoPollingMode)
104
+ @stopped.set
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ # :return [ConfigEntry, String] Returns the ConfigEntry object and error message in case of any error.
111
+ def fetch_if_older(time, prefer_cache: false)
112
+ # Sync up with the cache and use it when it's not expired.
113
+ @lock.synchronize do
114
+ if @cached_entry.empty? || @cached_entry.fetch_time > time
115
+ entry = read_cache
116
+ if !entry.empty? && entry.etag != @cached_entry.etag
117
+ @cached_entry = entry
118
+ @hooks.invoke_on_config_changed(entry.config[FEATURE_FLAGS])
119
+ end
120
+
121
+ # Cache isn't expired
122
+ if @cached_entry.fetch_time > time
123
+ set_initialized
124
+ return @cached_entry, nil
125
+ end
126
+ end
127
+
128
+ # Use cache anyway (get calls on auto & manual poll must not initiate fetch).
129
+ # The initialized check ensures that we subscribe for the ongoing fetch during the
130
+ # max init wait time window in case of auto poll.
131
+ if prefer_cache && @initialized.set?
132
+ return @cached_entry, nil
133
+ end
134
+
135
+ # If we are in offline mode we are not allowed to initiate fetch.
136
+ if @is_offline
137
+ offline_warning = "Client is in offline mode, it cannot initiate HTTP calls."
138
+ @log.warn(3200, offline_warning)
139
+ return @cached_entry, offline_warning
140
+ end
141
+ end
142
+
143
+ # No fetch is running, initiate a new one.
144
+ # Ensure only one fetch request is running at a time.
145
+ # If there's an ongoing fetch running, we will wait for the ongoing fetch.
146
+ if @ongoing_fetch
147
+ @fetch_finished.wait
148
+ else
149
+ @ongoing_fetch = true
150
+ @fetch_finished.reset
151
+ response = @config_fetcher.get_configuration(@cached_entry.etag)
152
+
153
+ @lock.synchronize do
154
+ if response.is_fetched
155
+ @cached_entry = response.entry
156
+ write_cache(response.entry)
157
+ @hooks.invoke_on_config_changed(response.entry.config[FEATURE_FLAGS])
158
+ elsif (response.is_not_modified || !response.is_transient_error) && !@cached_entry.empty?
159
+ @cached_entry.fetch_time = Utils.get_utc_now_seconds_since_epoch
160
+ write_cache(@cached_entry)
161
+ end
162
+
163
+ set_initialized
164
+ end
165
+
166
+ @ongoing_fetch = false
167
+ @fetch_finished.set
168
+ end
169
+
170
+ return @cached_entry, nil
171
+ end
172
+
173
+ def start_poll
174
+ @started = Concurrent::Event.new
175
+ @thread = Thread.new { run() }
176
+ @started.wait()
177
+ end
178
+
179
+ def run
180
+ @stopped = Concurrent::Event.new
181
+ @started.set
182
+ loop do
183
+ fetch_if_older(Utils.get_utc_now_seconds_since_epoch - @polling_mode.poll_interval_seconds)
184
+ @stopped.wait(@polling_mode.poll_interval_seconds)
185
+ break if @stopped.set?
186
+ end
187
+ end
188
+
189
+ def set_initialized
190
+ if !@initialized.set?
191
+ @initialized.set
192
+ @hooks.invoke_on_client_ready
193
+ end
194
+ end
195
+
196
+ def read_cache
197
+ begin
198
+ json_string = @config_cache.get(@cache_key)
199
+ if !json_string || json_string == @cached_entry_string
200
+ return ConfigEntry::EMPTY
201
+ end
202
+
203
+ @cached_entry_string = json_string
204
+ return ConfigEntry.create_from_json(JSON.parse(json_string))
205
+ rescue Exception => e
206
+ @log.error(2200, "Error occurred while reading the cache. #{e}")
207
+ return ConfigEntry::EMPTY
208
+ end
209
+ end
210
+
211
+ def write_cache(config_entry)
212
+ begin
213
+ @config_cache.set(@cache_key, config_entry.to_json.to_json)
214
+ rescue Exception => e
215
+ @log.error(2201, "Error occurred while writing the cache. #{e}")
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,23 @@
1
+ module ConfigCat
2
+ class EvaluationDetails
3
+ attr_reader :key, :value, :variation_id, :fetch_time, :user, :is_default_value, :error,
4
+ :matched_evaluation_rule, :matched_evaluation_percentage_rule
5
+
6
+ def initialize(key:, value:, variation_id: nil, fetch_time: nil, user: nil, is_default_value: false, error: nil,
7
+ matched_evaluation_rule: nil, matched_evaluation_percentage_rule: nil)
8
+ @key = key
9
+ @value = value
10
+ @variation_id = variation_id
11
+ @fetch_time = fetch_time
12
+ @user = user
13
+ @is_default_value = is_default_value
14
+ @error = error
15
+ @matched_evaluation_rule = matched_evaluation_rule
16
+ @matched_evaluation_percentage_rule = matched_evaluation_percentage_rule
17
+ end
18
+
19
+ def self.from_error(key, value, error:, variation_id: nil)
20
+ EvaluationDetails.new(key: key, value: value, variation_id: variation_id, is_default_value: true, error: error)
21
+ end
22
+ end
23
+ end