configcat 5.0.2 → 6.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/configcat/configcache.rb +19 -4
- data/lib/configcat/configcatclient.rb +278 -111
- 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 +99 -40
- data/lib/configcat/configservice.rb +219 -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 +39 -39
- 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,21 @@ 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(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
|
-
|
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
|
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
|
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
|