configcat 5.0.2 → 6.0.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.
- checksums.yaml +4 -4
- data/lib/configcat/configcache.rb +19 -4
- data/lib/configcat/configcatclient.rb +263 -107
- data/lib/configcat/configcatlogger.rb +24 -0
- data/lib/configcat/configcatoptions.rb +153 -0
- data/lib/configcat/configentry.rb +40 -0
- data/lib/configcat/configfetcher.rb +98 -40
- data/lib/configcat/configservice.rb +212 -0
- data/lib/configcat/evaluationdetails.rb +23 -0
- data/lib/configcat/interfaces.rb +4 -59
- data/lib/configcat/localdictionarydatasource.rb +14 -4
- data/lib/configcat/localfiledatasource.rb +22 -11
- data/lib/configcat/overridedatasource.rb +8 -2
- data/lib/configcat/pollingmode.rb +62 -0
- data/lib/configcat/refreshresult.rb +3 -0
- data/lib/configcat/rolloutevaluator.rb +32 -37
- data/lib/configcat/user.rb +18 -39
- data/lib/configcat/utils.rb +10 -0
- data/lib/configcat/version.rb +1 -1
- data/lib/configcat.rb +128 -136
- metadata +24 -5
- data/lib/configcat/autopollingcachepolicy.rb +0 -99
- data/lib/configcat/lazyloadingcachepolicy.rb +0 -69
- data/lib/configcat/manualpollingcachepolicy.rb +0 -47
@@ -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
|
-
|
23
|
-
|
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
|
-
#
|
27
|
-
|
28
|
-
|
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
|
-
|
32
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|
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
|
-
@
|
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
|
78
|
-
def
|
79
|
-
|
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.
|
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
|
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,20 @@ module ConfigCat
|
|
124
140
|
# Try to download again with the new url
|
125
141
|
|
126
142
|
if redirect == RedirectMode::SHOULD_REDIRECT
|
127
|
-
|
143
|
+
@log.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.")
|
128
144
|
end
|
129
145
|
|
130
146
|
# To prevent loops we check if we retried at least 3 times with the new base_url
|
131
147
|
if retries >= 2
|
132
|
-
|
148
|
+
@log.error("Redirect loop during config.json fetch. Please contact support@configcat.com.")
|
133
149
|
return fetch_response
|
134
150
|
end
|
135
151
|
|
136
152
|
# Retry the config download with the new base_url
|
137
|
-
return
|
153
|
+
return get_configuration(etag, retries + 1)
|
138
154
|
end
|
139
155
|
|
140
|
-
def close
|
156
|
+
def close
|
141
157
|
if @_http
|
142
158
|
@_http = nil
|
143
159
|
end
|
@@ -145,7 +161,49 @@ module ConfigCat
|
|
145
161
|
|
146
162
|
private
|
147
163
|
|
148
|
-
def
|
164
|
+
def _fetch(etag)
|
165
|
+
begin
|
166
|
+
@log.debug("Fetching configuration from ConfigCat")
|
167
|
+
uri = URI.parse((((@_base_url + ("/")) + BASE_PATH) + @_sdk_key) + BASE_EXTENSION)
|
168
|
+
headers = @_headers
|
169
|
+
headers["If-None-Match"] = etag.empty? ? nil : etag
|
170
|
+
_create_http()
|
171
|
+
request = Net::HTTP::Get.new(uri.request_uri, headers)
|
172
|
+
response = @_http.request(request)
|
173
|
+
case response
|
174
|
+
when Net::HTTPSuccess
|
175
|
+
@log.debug("ConfigCat configuration json fetch response code:#{response.code} Cached:#{response['ETag']}")
|
176
|
+
response_etag = response["ETag"]
|
177
|
+
if response_etag.nil?
|
178
|
+
response_etag = ""
|
179
|
+
end
|
180
|
+
config = JSON.parse(response.body)
|
181
|
+
return FetchResponse.success(ConfigEntry.new(config, response_etag, Utils.get_utc_now_seconds_since_epoch))
|
182
|
+
when Net::HTTPNotModified
|
183
|
+
return FetchResponse.not_modified
|
184
|
+
when Net::HTTPNotFound, Net::HTTPForbidden
|
185
|
+
error = "Double-check your SDK Key at https://app.configcat.com/sdkkey. Received unexpected response: #{response}"
|
186
|
+
@log.error(error)
|
187
|
+
return FetchResponse.failure(error, false)
|
188
|
+
else
|
189
|
+
raise Net::HTTPError.new("", response)
|
190
|
+
end
|
191
|
+
rescue Net::HTTPError => e
|
192
|
+
error = "Unexpected HTTP response was received: #{e}"
|
193
|
+
@log.error(error)
|
194
|
+
return FetchResponse.failure(error, true)
|
195
|
+
rescue Timeout::Error => e
|
196
|
+
error = "Request timed out. Timeout values: [connect: #{get_open_timeout()}s, read: #{get_read_timeout()}s]"
|
197
|
+
@log.error(error)
|
198
|
+
return FetchResponse.failure(error, true)
|
199
|
+
rescue Exception => e
|
200
|
+
error = "An exception occurred during fetching: #{e}"
|
201
|
+
@log.error(error)
|
202
|
+
return FetchResponse.failure(error, true)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def _create_http
|
149
207
|
uri = URI.parse(@_base_url)
|
150
208
|
use_ssl = true if uri.scheme == 'https'
|
151
209
|
if @_http.equal?(nil) || @_http.address != uri.host || @_http.port != uri.port || @_http.use_ssl? != use_ssl
|
@@ -0,0 +1,212 @@
|
|
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)
|
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.config[FEATURE_FLAGS], entry.fetch_time
|
38
|
+
elsif @polling_mode.is_a?(AutoPollingMode) && !@initialized.set?
|
39
|
+
elapsed_time = Utils.get_utc_now_seconds_since_epoch - @start_time # Elapsed time in seconds
|
40
|
+
if elapsed_time < @polling_mode.max_init_wait_time_seconds
|
41
|
+
@initialized.wait(@polling_mode.max_init_wait_time_seconds - elapsed_time)
|
42
|
+
|
43
|
+
# Max wait time expired without result, notify subscribers with the cached config.
|
44
|
+
if !@initialized.set?
|
45
|
+
set_initialized
|
46
|
+
return @cached_entry.config[FEATURE_FLAGS], @cached_entry.fetch_time
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
entry, _ = fetch_if_older(Utils::DISTANT_PAST, prefer_cache: true)
|
52
|
+
return entry.config[FEATURE_FLAGS], entry.fetch_time
|
53
|
+
end
|
54
|
+
|
55
|
+
# :return [RefreshResult]
|
56
|
+
def refresh
|
57
|
+
_, error = fetch_if_older(Utils::DISTANT_FUTURE)
|
58
|
+
return RefreshResult.new(success = error.nil?, error = error)
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_online
|
62
|
+
@lock.synchronize do
|
63
|
+
if !@is_offline
|
64
|
+
return
|
65
|
+
end
|
66
|
+
|
67
|
+
@is_offline = false
|
68
|
+
if @polling_mode.is_a?(AutoPollingMode)
|
69
|
+
start_poll
|
70
|
+
end
|
71
|
+
@log.debug('Switched to ONLINE mode.')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def set_offline
|
76
|
+
@lock.synchronize do
|
77
|
+
if @is_offline
|
78
|
+
return
|
79
|
+
end
|
80
|
+
|
81
|
+
@is_offline = true
|
82
|
+
if @polling_mode.is_a?(AutoPollingMode)
|
83
|
+
@stopped.set
|
84
|
+
@thread.join
|
85
|
+
end
|
86
|
+
|
87
|
+
@log.debug('Switched to OFFLINE mode.')
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def offline?
|
92
|
+
return @is_offline
|
93
|
+
end
|
94
|
+
|
95
|
+
def close
|
96
|
+
if @polling_mode.is_a?(AutoPollingMode)
|
97
|
+
@stopped.set
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# :return [ConfigEntry, String] Returns the ConfigEntry object and error message in case of any error.
|
104
|
+
def fetch_if_older(time, prefer_cache: false)
|
105
|
+
# Sync up with the cache and use it when it's not expired.
|
106
|
+
@lock.synchronize do
|
107
|
+
if @cached_entry.empty? || @cached_entry.fetch_time > time
|
108
|
+
entry = read_cache
|
109
|
+
if !entry.empty? && entry.etag != @cached_entry.etag
|
110
|
+
@cached_entry = entry
|
111
|
+
@hooks.invoke_on_config_changed(entry.config[FEATURE_FLAGS])
|
112
|
+
end
|
113
|
+
|
114
|
+
# Cache isn't expired
|
115
|
+
if @cached_entry.fetch_time > time
|
116
|
+
set_initialized
|
117
|
+
return @cached_entry, nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Use cache anyway (get calls on auto & manual poll must not initiate fetch).
|
122
|
+
# The initialized check ensures that we subscribe for the ongoing fetch during the
|
123
|
+
# max init wait time window in case of auto poll.
|
124
|
+
if prefer_cache && @initialized.set?
|
125
|
+
return @cached_entry, nil
|
126
|
+
end
|
127
|
+
|
128
|
+
# If we are in offline mode we are not allowed to initiate fetch.
|
129
|
+
if @is_offline
|
130
|
+
offline_warning = 'The SDK is in offline mode, it can not initiate HTTP calls.'
|
131
|
+
@log.warn(offline_warning)
|
132
|
+
return @cached_entry, offline_warning
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# No fetch is running, initiate a new one.
|
137
|
+
# Ensure only one fetch request is running at a time.
|
138
|
+
# If there's an ongoing fetch running, we will wait for the ongoing fetch.
|
139
|
+
if @ongoing_fetch
|
140
|
+
@fetch_finished.wait
|
141
|
+
else
|
142
|
+
@ongoing_fetch = true
|
143
|
+
@fetch_finished.reset
|
144
|
+
response = @config_fetcher.get_configuration(@cached_entry.etag)
|
145
|
+
|
146
|
+
@lock.synchronize do
|
147
|
+
if response.is_fetched
|
148
|
+
@cached_entry = response.entry
|
149
|
+
write_cache(response.entry)
|
150
|
+
@hooks.invoke_on_config_changed(response.entry.config[FEATURE_FLAGS])
|
151
|
+
elsif (response.is_not_modified || !response.is_transient_error) && !@cached_entry.empty?
|
152
|
+
@cached_entry.fetch_time = Utils.get_utc_now_seconds_since_epoch
|
153
|
+
write_cache(@cached_entry)
|
154
|
+
end
|
155
|
+
|
156
|
+
set_initialized
|
157
|
+
end
|
158
|
+
|
159
|
+
@ongoing_fetch = false
|
160
|
+
@fetch_finished.set
|
161
|
+
end
|
162
|
+
|
163
|
+
return @cached_entry, nil
|
164
|
+
end
|
165
|
+
|
166
|
+
def start_poll
|
167
|
+
@started = Concurrent::Event.new
|
168
|
+
@thread = Thread.new { run() }
|
169
|
+
@started.wait()
|
170
|
+
end
|
171
|
+
|
172
|
+
def run
|
173
|
+
@stopped = Concurrent::Event.new
|
174
|
+
@started.set
|
175
|
+
loop do
|
176
|
+
fetch_if_older(Utils.get_utc_now_seconds_since_epoch - @polling_mode.poll_interval_seconds)
|
177
|
+
@stopped.wait(@polling_mode.poll_interval_seconds)
|
178
|
+
break if @stopped.set?
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def set_initialized
|
183
|
+
if !@initialized.set?
|
184
|
+
@initialized.set
|
185
|
+
@hooks.invoke_on_client_ready
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def read_cache
|
190
|
+
begin
|
191
|
+
json_string = @config_cache.get(@cache_key)
|
192
|
+
if !json_string || json_string == @cached_entry_string
|
193
|
+
return ConfigEntry::EMPTY
|
194
|
+
end
|
195
|
+
|
196
|
+
@cached_entry_string = json_string
|
197
|
+
return ConfigEntry.create_from_json(JSON.parse(json_string))
|
198
|
+
rescue Exception => e
|
199
|
+
@log.error("An error occurred during the cache read. #{e}")
|
200
|
+
return ConfigEntry::EMPTY
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def write_cache(config_entry)
|
205
|
+
begin
|
206
|
+
@config_cache.set(@cache_key, config_entry.to_json.to_json)
|
207
|
+
rescue Exception => e
|
208
|
+
@log.error("An error occurred during the cache write. #{e}")
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
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
|