configcat 5.0.2 → 6.1.0

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.
@@ -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