prefab-cloud-ruby 0.13.1 → 0.13.2
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/.github/workflows/ruby.yml +39 -0
- data/.tool-versions +1 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +11 -4
- data/Gemfile.lock +52 -32
- data/README.md +19 -1
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/compile_protos.sh +3 -0
- data/lib/prefab/auth_interceptor.rb +1 -0
- data/lib/prefab/cancellable_interceptor.rb +1 -0
- data/lib/prefab/client.rb +51 -38
- data/lib/prefab/config_client.rb +145 -73
- data/lib/prefab/config_helper.rb +29 -0
- data/lib/prefab/config_loader.rb +98 -13
- data/lib/prefab/config_resolver.rb +56 -49
- data/lib/prefab/error.rb +6 -0
- data/lib/prefab/errors/initialization_timeout_error.rb +13 -0
- data/lib/prefab/errors/invalid_api_key_error.rb +19 -0
- data/lib/prefab/errors/missing_default_error.rb +13 -0
- data/lib/prefab/feature_flag_client.rb +129 -11
- data/lib/prefab/internal_logger.rb +29 -0
- data/lib/prefab/logger_client.rb +10 -8
- data/lib/prefab/murmer3.rb +1 -0
- data/lib/prefab/noop_cache.rb +1 -0
- data/lib/prefab/noop_stats.rb +1 -0
- data/lib/prefab/options.rb +82 -0
- data/lib/prefab/ratelimit_client.rb +1 -0
- data/lib/prefab-cloud-ruby.rb +10 -0
- data/lib/prefab_pb.rb +214 -132
- data/lib/prefab_services_pb.rb +35 -6
- data/prefab-cloud-ruby.gemspec +29 -10
- data/run_test_harness_server.sh +8 -0
- data/test/.prefab.test.config.yaml +27 -1
- data/test/harness_server.rb +64 -0
- data/test/test_client.rb +98 -0
- data/test/test_config_client.rb +56 -0
- data/test/test_config_loader.rb +39 -25
- data/test/test_config_resolver.rb +134 -38
- data/test/test_feature_flag_client.rb +277 -35
- data/test/test_helper.rb +70 -4
- data/test/test_logger.rb +23 -29
- metadata +69 -14
- data/.ruby-version +0 -1
data/lib/prefab/config_client.rb
CHANGED
@@ -1,36 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Prefab
|
2
3
|
class ConfigClient
|
4
|
+
include Prefab::ConfigHelper
|
5
|
+
|
3
6
|
RECONNECT_WAIT = 5
|
4
7
|
DEFAULT_CHECKPOINT_FREQ_SEC = 60
|
5
8
|
DEFAULT_S3CF_BUCKET = 'http://d2j4ed6ti5snnd.cloudfront.net'
|
9
|
+
SSE_READ_TIMEOUT = 300
|
6
10
|
|
7
11
|
def initialize(base_client, timeout)
|
8
12
|
@base_client = base_client
|
13
|
+
@options = base_client.options
|
14
|
+
@base_client.log_internal Logger::DEBUG, "Initialize ConfigClient"
|
9
15
|
@timeout = timeout
|
10
|
-
|
16
|
+
|
17
|
+
@stream_lock = Concurrent::ReadWriteLock.new
|
11
18
|
|
12
19
|
@checkpoint_freq_secs = DEFAULT_CHECKPOINT_FREQ_SEC
|
13
20
|
|
14
21
|
@config_loader = Prefab::ConfigLoader.new(@base_client)
|
15
22
|
@config_resolver = Prefab::ConfigResolver.new(@base_client, @config_loader)
|
16
23
|
|
24
|
+
@initialization_lock = Concurrent::ReadWriteLock.new
|
25
|
+
@base_client.log_internal Logger::DEBUG, "Initialize ConfigClient: AcquireWriteLock"
|
17
26
|
@initialization_lock.acquire_write_lock
|
27
|
+
@base_client.log_internal Logger::DEBUG, "Initialize ConfigClient: AcquiredWriteLock"
|
28
|
+
@initialized_future = Concurrent::Future.execute { @initialization_lock.acquire_read_lock }
|
18
29
|
|
19
30
|
@cancellable_interceptor = Prefab::CancellableInterceptor.new(@base_client)
|
20
31
|
|
21
32
|
@s3_cloud_front = ENV["PREFAB_S3CF_BUCKET"] || DEFAULT_S3CF_BUCKET
|
22
|
-
load_checkpoint
|
23
|
-
start_checkpointing_thread
|
24
|
-
end
|
25
33
|
|
26
|
-
|
27
|
-
|
28
|
-
|
34
|
+
if @options.local_only?
|
35
|
+
finish_init!(:local_only)
|
36
|
+
else
|
37
|
+
load_checkpoint
|
38
|
+
start_checkpointing_thread
|
39
|
+
start_streaming
|
40
|
+
end
|
29
41
|
end
|
30
42
|
|
31
|
-
def
|
32
|
-
@
|
33
|
-
@
|
43
|
+
def start_streaming
|
44
|
+
@stream_lock.with_write_lock do
|
45
|
+
start_sse_streaming_connection_thread(@config_loader.highwater_mark) if @streaming_thread.nil?
|
34
46
|
end
|
35
47
|
end
|
36
48
|
|
@@ -43,7 +55,7 @@ module Prefab
|
|
43
55
|
|
44
56
|
@base_client.request Prefab::ConfigService, :upsert, req_options: { timeout: @timeout }, params: upsert_req
|
45
57
|
@base_client.stats.increment("prefab.config.upsert")
|
46
|
-
@config_loader.set(config_delta)
|
58
|
+
@config_loader.set(config_delta, :upsert)
|
47
59
|
@config_loader.rm(previous_key) if previous_key&.present?
|
48
60
|
@config_resolver.update
|
49
61
|
end
|
@@ -58,12 +70,48 @@ module Prefab
|
|
58
70
|
end
|
59
71
|
|
60
72
|
def self.value_to_delta(key, config_value, namespace = nil)
|
61
|
-
Prefab::
|
62
|
-
|
73
|
+
Prefab::Config.new(key: [namespace, key].compact.join(":"),
|
74
|
+
rows: [Prefab::ConfigRow.new(value: config_value)])
|
75
|
+
end
|
76
|
+
|
77
|
+
def get(key, default=Prefab::Client::NO_DEFAULT_PROVIDED)
|
78
|
+
config = _get(key)
|
79
|
+
config ? value_of(config[:value]) : handle_default(key, default)
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_config_obj(key)
|
83
|
+
config = _get(key)
|
84
|
+
config ? config[:config] : nil
|
63
85
|
end
|
64
86
|
|
65
87
|
private
|
66
88
|
|
89
|
+
def handle_default(key, default)
|
90
|
+
if default != Prefab::Client::NO_DEFAULT_PROVIDED
|
91
|
+
return default
|
92
|
+
end
|
93
|
+
|
94
|
+
if @options.on_no_default == Prefab::Options::ON_NO_DEFAULT::RAISE
|
95
|
+
raise Prefab::Errors::MissingDefaultError.new(key)
|
96
|
+
end
|
97
|
+
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
|
101
|
+
def _get(key)
|
102
|
+
# wait timeout sec for the initalization to be complete
|
103
|
+
@initialized_future.value(@options.initialization_timeout_sec)
|
104
|
+
if @initialized_future.incomplete?
|
105
|
+
if @options.on_init_failure == Prefab::Options::ON_INITIALIZATION_FAILURE::RETURN
|
106
|
+
@base_client.log_internal Logger::WARN, "Couldn't Initialize In #{@options.initialization_timeout_sec}. Key #{key}. Returning what we have"
|
107
|
+
@initialization_lock.release_write_lock
|
108
|
+
else
|
109
|
+
raise Prefab::Errors::InitializationTimeoutError.new(@options.initialization_timeout_sec, key)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
@config_resolver._get(key)
|
113
|
+
end
|
114
|
+
|
67
115
|
def stub
|
68
116
|
@_stub = Prefab::ConfigService::Stub.new(nil,
|
69
117
|
nil,
|
@@ -71,53 +119,92 @@ module Prefab
|
|
71
119
|
interceptors: [@base_client.interceptor, @cancellable_interceptor])
|
72
120
|
end
|
73
121
|
|
74
|
-
#
|
75
|
-
# returns the high-watermark of what was in the cache
|
122
|
+
# try API first, if not, fallback to s3
|
76
123
|
def load_checkpoint
|
77
|
-
success =
|
124
|
+
success = load_checkpoint_api_cdn
|
78
125
|
|
79
|
-
if
|
80
|
-
|
81
|
-
|
126
|
+
if success
|
127
|
+
return
|
128
|
+
else
|
129
|
+
@base_client.log_internal Logger::INFO, "LoadCheckpoint: Fallback to GRPC API"
|
82
130
|
end
|
83
131
|
|
84
|
-
|
85
|
-
|
132
|
+
success = load_checkpoint_from_grpc_api
|
133
|
+
|
134
|
+
if success
|
135
|
+
return
|
136
|
+
else
|
137
|
+
@base_client.log_internal Logger::INFO, "LoadCheckpoint: Fallback to S3"
|
138
|
+
end
|
139
|
+
|
140
|
+
success = load_checkpoint_from_s3
|
141
|
+
|
142
|
+
if !success
|
143
|
+
@base_client.log_internal Logger::WARN, "No success loading checkpoints"
|
144
|
+
end
|
86
145
|
end
|
87
146
|
|
88
|
-
def
|
89
|
-
config_req = Prefab::ConfigServicePointer.new(
|
90
|
-
|
147
|
+
def load_checkpoint_from_grpc_api
|
148
|
+
config_req = Prefab::ConfigServicePointer.new(start_at_id: @config_loader.highwater_mark)
|
149
|
+
|
91
150
|
resp = stub.get_all_config(config_req)
|
92
|
-
|
93
|
-
resp.deltas.each do |delta|
|
94
|
-
@config_loader.set(delta)
|
95
|
-
end
|
96
|
-
@config_resolver.update
|
97
|
-
finish_init!(:api)
|
151
|
+
load_configs(resp, :remote_api_grpc)
|
98
152
|
true
|
153
|
+
rescue GRPC::Unauthenticated
|
154
|
+
@base_client.log_internal Logger::WARN, "Unauthenticated"
|
99
155
|
rescue => e
|
100
|
-
@base_client.log_internal Logger::WARN, "Unexpected problem loading checkpoint #{e}"
|
156
|
+
@base_client.log_internal Logger::WARN, "Unexpected grpc_api problem loading checkpoint #{e}"
|
101
157
|
false
|
102
158
|
end
|
103
159
|
|
160
|
+
def load_checkpoint_api_cdn
|
161
|
+
key_hash = Murmur3.murmur3_32(@base_client.api_key)
|
162
|
+
url = "#{@options.url_for_api_cdn}/api/v1/config/#{@base_client.project_id}/#{key_hash}/0"
|
163
|
+
conn = if Faraday::VERSION[0].to_i >= 2
|
164
|
+
Faraday.new(url) do |conn|
|
165
|
+
conn.request :authorization, :basic, @base_client.project_id, @base_client.api_key
|
166
|
+
end
|
167
|
+
else
|
168
|
+
Faraday.new(url) do |conn|
|
169
|
+
conn.request :basic_auth, @base_client.project_id, @base_client.api_key
|
170
|
+
end
|
171
|
+
end
|
172
|
+
load_url(conn, :remote_cdn_api)
|
173
|
+
end
|
174
|
+
|
104
175
|
def load_checkpoint_from_s3
|
105
176
|
url = "#{@s3_cloud_front}/#{@base_client.api_key.gsub("|", "/")}"
|
106
|
-
|
177
|
+
load_url(Faraday.new(url), :remote_s3)
|
178
|
+
end
|
179
|
+
|
180
|
+
def load_url(conn, source)
|
181
|
+
resp = conn.get('')
|
107
182
|
if resp.status == 200
|
108
|
-
|
109
|
-
|
183
|
+
configs = Prefab::Configs.decode(resp.body)
|
184
|
+
load_configs(configs, source)
|
185
|
+
true
|
110
186
|
else
|
111
|
-
@base_client.log_internal Logger::INFO, "
|
187
|
+
@base_client.log_internal Logger::INFO, "Checkpoint #{source} failed to load. Response #{resp.status}"
|
188
|
+
false
|
112
189
|
end
|
190
|
+
rescue => e
|
191
|
+
@base_client.log_internal Logger::WARN, "Unexpected #{source} problem loading checkpoint #{e} #{conn}"
|
192
|
+
false
|
113
193
|
end
|
114
194
|
|
195
|
+
def load_configs(configs, source)
|
196
|
+
project_env_id = configs.config_service_pointer.project_env_id
|
197
|
+
@config_resolver.project_env_id = project_env_id
|
198
|
+
starting_highwater_mark = @config_loader.highwater_mark
|
115
199
|
|
116
|
-
|
117
|
-
|
118
|
-
|
200
|
+
configs.configs.each do |config|
|
201
|
+
@config_loader.set(config, source)
|
202
|
+
end
|
203
|
+
if @config_loader.highwater_mark > starting_highwater_mark
|
204
|
+
@base_client.log_internal Logger::INFO, "Found new checkpoint with highwater id #{@config_loader.highwater_mark} from #{source} in project #{@base_client.project_id} environment: #{project_env_id} and namespace: '#{@namespace}'"
|
205
|
+
else
|
206
|
+
@base_client.log_internal Logger::DEBUG, "Checkpoint with highwater id #{@config_loader.highwater_mark} from #{source}. No changes.", "prefab.config_client.load_configs"
|
119
207
|
end
|
120
|
-
@base_client.log_internal Logger::INFO, "Found checkpoint with highwater id #{@config_loader.highwater_mark} from #{source}"
|
121
208
|
@base_client.stats.increment("prefab.config.checkpoint.load")
|
122
209
|
@config_resolver.update
|
123
210
|
finish_init!(source)
|
@@ -125,6 +212,7 @@ module Prefab
|
|
125
212
|
|
126
213
|
# A thread that checks for a checkpoint
|
127
214
|
def start_checkpointing_thread
|
215
|
+
|
128
216
|
Thread.new do
|
129
217
|
loop do
|
130
218
|
begin
|
@@ -144,47 +232,31 @@ module Prefab
|
|
144
232
|
|
145
233
|
def finish_init!(source)
|
146
234
|
if @initialization_lock.write_locked?
|
147
|
-
@base_client.log_internal Logger::
|
235
|
+
@base_client.log_internal Logger::INFO, "Unlocked Config via #{source}"
|
148
236
|
@initialization_lock.release_write_lock
|
149
237
|
@base_client.log.set_config_client(self)
|
238
|
+
@base_client.log_internal Logger::INFO, to_s
|
150
239
|
end
|
151
240
|
end
|
152
241
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
@
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
resp = stub.get_config(config_req)
|
170
|
-
resp.each do |r|
|
171
|
-
r.deltas.each do |delta|
|
172
|
-
@config_loader.set(delta)
|
173
|
-
end
|
174
|
-
@config_resolver.update
|
175
|
-
finish_init!(:streaming)
|
176
|
-
end
|
177
|
-
rescue => e
|
178
|
-
if @streaming
|
179
|
-
level = e.code == 1 ? Logger::DEBUG : Logger::INFO
|
180
|
-
@base_client.log_internal level, ("config client encountered #{e.message} pausing #{RECONNECT_WAIT}")
|
181
|
-
reset
|
182
|
-
sleep(RECONNECT_WAIT)
|
183
|
-
end
|
184
|
-
end
|
242
|
+
def start_sse_streaming_connection_thread(start_at_id)
|
243
|
+
auth = "#{@base_client.project_id}:#{@base_client.api_key}"
|
244
|
+
auth_string = Base64.strict_encode64(auth)
|
245
|
+
headers = {
|
246
|
+
"x-prefab-start-at-id": start_at_id,
|
247
|
+
"Authorization": "Basic #{auth_string}",
|
248
|
+
}
|
249
|
+
url = "#{@base_client.prefab_api_url}/api/v1/sse/config"
|
250
|
+
@base_client.log_internal Logger::INFO, "SSE Streaming Connect to #{url} start_at #{start_at_id}"
|
251
|
+
@streaming_thread = SSE::Client.new(url,
|
252
|
+
headers: headers,
|
253
|
+
read_timeout: SSE_READ_TIMEOUT,
|
254
|
+
logger: Prefab::InternalLogger.new("prefab.config.sse", @base_client.log)) do |client|
|
255
|
+
client.on_event do |event|
|
256
|
+
configs = Prefab::Configs.decode(Base64.decode64(event.data))
|
257
|
+
load_configs(configs, :sse)
|
185
258
|
end
|
186
259
|
end
|
187
|
-
|
188
260
|
end
|
189
261
|
end
|
190
262
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Prefab
|
3
|
+
module ConfigHelper
|
4
|
+
def value_of(config_value)
|
5
|
+
case config_value.type
|
6
|
+
when :string
|
7
|
+
config_value.string
|
8
|
+
when :int
|
9
|
+
config_value.int
|
10
|
+
when :double
|
11
|
+
config_value.double
|
12
|
+
when :bool
|
13
|
+
config_value.bool
|
14
|
+
when :feature_flag
|
15
|
+
config_value.feature_flag
|
16
|
+
when :segment
|
17
|
+
config_value.segment
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def value_of_variant(feature_flag_variant)
|
22
|
+
return feature_flag_variant.string if feature_flag_variant.has_string?
|
23
|
+
return feature_flag_variant.int if feature_flag_variant.has_int?
|
24
|
+
return feature_flag_variant.double if feature_flag_variant.has_double?
|
25
|
+
return feature_flag_variant.bool if feature_flag_variant.has_bool?
|
26
|
+
return nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/prefab/config_loader.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'yaml'
|
2
4
|
module Prefab
|
3
5
|
class ConfigLoader
|
@@ -5,6 +7,7 @@ module Prefab
|
|
5
7
|
|
6
8
|
def initialize(base_client)
|
7
9
|
@base_client = base_client
|
10
|
+
@prefab_options = base_client.options
|
8
11
|
@highwater_mark = 0
|
9
12
|
@classpath_config = load_classpath_config
|
10
13
|
@local_overrides = load_local_overrides
|
@@ -14,24 +17,27 @@ module Prefab
|
|
14
17
|
def calc_config
|
15
18
|
rtn = @classpath_config.clone
|
16
19
|
@api_config.each_key do |k|
|
17
|
-
rtn[k] = @api_config[k]
|
20
|
+
rtn[k] = @api_config[k]
|
18
21
|
end
|
19
22
|
rtn = rtn.merge(@local_overrides)
|
20
23
|
rtn
|
21
24
|
end
|
22
25
|
|
23
|
-
def set(
|
26
|
+
def set(config, source)
|
24
27
|
# don't overwrite newer values
|
25
|
-
if @api_config[
|
28
|
+
if @api_config[config.key] && @api_config[config.key][:config].id >= config.id
|
26
29
|
return
|
27
30
|
end
|
28
31
|
|
29
|
-
if
|
30
|
-
@api_config.delete(
|
32
|
+
if config.rows.empty?
|
33
|
+
@api_config.delete(config.key)
|
31
34
|
else
|
32
|
-
@api_config[
|
35
|
+
if @api_config[config.key]
|
36
|
+
@base_client.log_internal Logger::DEBUG, "Replace #{config.key} with value from #{source} #{ @api_config[config.key][:config].id} -> #{config.id}"
|
37
|
+
end
|
38
|
+
@api_config[config.key] = { source: source, config: config }
|
33
39
|
end
|
34
|
-
@highwater_mark = [
|
40
|
+
@highwater_mark = [config.id, @highwater_mark].max
|
35
41
|
end
|
36
42
|
|
37
43
|
def rm(key)
|
@@ -39,22 +45,22 @@ module Prefab
|
|
39
45
|
end
|
40
46
|
|
41
47
|
def get_api_deltas
|
42
|
-
|
48
|
+
configs = Prefab::Configs.new
|
43
49
|
@api_config.each_value do |config_value|
|
44
|
-
|
50
|
+
configs.configs << config_value[:config]
|
45
51
|
end
|
46
|
-
|
52
|
+
configs
|
47
53
|
end
|
48
54
|
|
49
55
|
private
|
50
56
|
|
51
57
|
def load_classpath_config
|
52
|
-
classpath_dir =
|
58
|
+
classpath_dir = @prefab_options.prefab_config_classpath_dir
|
53
59
|
load_glob(File.join(classpath_dir, ".prefab*config.yaml"))
|
54
60
|
end
|
55
61
|
|
56
62
|
def load_local_overrides
|
57
|
-
override_dir =
|
63
|
+
override_dir = @prefab_options.prefab_config_override_dir
|
58
64
|
load_glob(File.join(override_dir, ".prefab*config.yaml"))
|
59
65
|
end
|
60
66
|
|
@@ -63,7 +69,39 @@ module Prefab
|
|
63
69
|
Dir.glob(glob).each do |file|
|
64
70
|
yaml = load(file)
|
65
71
|
yaml.each do |k, v|
|
66
|
-
|
72
|
+
if v.class == Hash
|
73
|
+
v.each do |env_k, env_v|
|
74
|
+
if k == @prefab_options.defaults_env
|
75
|
+
if env_v.class == Hash && env_v['feature_flag']
|
76
|
+
rtn[env_k] = feature_flag_config(file, k, env_k, env_v)
|
77
|
+
else
|
78
|
+
rtn[env_k] = {
|
79
|
+
source: file,
|
80
|
+
match: k,
|
81
|
+
config: Prefab::Config.new(
|
82
|
+
key: env_k,
|
83
|
+
rows: [
|
84
|
+
Prefab::ConfigRow.new(value: Prefab::ConfigValue.new(value_from(env_v)))
|
85
|
+
]
|
86
|
+
)
|
87
|
+
}
|
88
|
+
end
|
89
|
+
else
|
90
|
+
next
|
91
|
+
end
|
92
|
+
end
|
93
|
+
else
|
94
|
+
rtn[k] = {
|
95
|
+
source: file,
|
96
|
+
match: "default",
|
97
|
+
config: Prefab::Config.new(
|
98
|
+
key: k,
|
99
|
+
rows: [
|
100
|
+
Prefab::ConfigRow.new(value: Prefab::ConfigValue.new(value_from(v)))
|
101
|
+
]
|
102
|
+
)
|
103
|
+
}
|
104
|
+
end
|
67
105
|
end
|
68
106
|
end
|
69
107
|
rtn
|
@@ -91,5 +129,52 @@ module Prefab
|
|
91
129
|
{ double: raw }
|
92
130
|
end
|
93
131
|
end
|
132
|
+
|
133
|
+
def feature_flag_config(file, k, env_k, env_v)
|
134
|
+
criteria = Prefab::Criteria.new(operator: 'ALWAYS_TRUE')
|
135
|
+
|
136
|
+
if env_v['criteria']
|
137
|
+
criteria = Prefab::Criteria.new(criteria_values(env_v['criteria']))
|
138
|
+
end
|
139
|
+
|
140
|
+
row = Prefab::ConfigRow.new(
|
141
|
+
value: Prefab::ConfigValue.new(
|
142
|
+
feature_flag: Prefab::FeatureFlag.new(
|
143
|
+
active: true,
|
144
|
+
inactive_variant_idx: -1, # not supported
|
145
|
+
rules: [
|
146
|
+
Prefab::Rule.new(
|
147
|
+
variant_weights: [
|
148
|
+
Prefab::VariantWeight.new(variant_idx: 0, weight: 1000)
|
149
|
+
],
|
150
|
+
criteria: criteria
|
151
|
+
)
|
152
|
+
]
|
153
|
+
)
|
154
|
+
)
|
155
|
+
)
|
156
|
+
|
157
|
+
unless env_v.has_key?('value')
|
158
|
+
raise Prefab::Error, "Feature flag config `#{env_k}` #{file} must have a `value`"
|
159
|
+
end
|
160
|
+
|
161
|
+
{
|
162
|
+
source: file,
|
163
|
+
match: k,
|
164
|
+
config: Prefab::Config.new(
|
165
|
+
key: env_k,
|
166
|
+
variants: [Prefab::FeatureFlagVariant.new(value_from(env_v['value']))],
|
167
|
+
rows: [row]
|
168
|
+
)
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
def criteria_values(criteria_hash)
|
173
|
+
if RUBY_VERSION < '2.7'
|
174
|
+
criteria_hash.transform_keys(&:to_sym)
|
175
|
+
else
|
176
|
+
criteria_hash
|
177
|
+
end
|
178
|
+
end
|
94
179
|
end
|
95
180
|
end
|
@@ -1,59 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Prefab
|
2
3
|
class ConfigResolver
|
3
|
-
|
4
|
-
|
4
|
+
include Prefab::ConfigHelper
|
5
|
+
NAMESPACE_DELIMITER = "."
|
6
|
+
|
7
|
+
attr_accessor :project_env_id # this will be set by the config_client when it gets an API response
|
5
8
|
|
6
9
|
def initialize(base_client, config_loader)
|
7
10
|
@lock = Concurrent::ReadWriteLock.new
|
8
11
|
@local_store = {}
|
9
|
-
@namespace = base_client.namespace
|
12
|
+
@namespace = base_client.options.namespace
|
10
13
|
@config_loader = config_loader
|
14
|
+
@project_env_id = 0
|
11
15
|
make_local
|
12
16
|
end
|
13
17
|
|
14
18
|
def to_s
|
15
|
-
str = ""
|
19
|
+
str = "\n"
|
16
20
|
@lock.with_read_lock do
|
17
21
|
@local_store.each do |k, v|
|
18
|
-
|
19
|
-
|
22
|
+
elements = [k.slice(0..49).ljust(50)]
|
23
|
+
if v.nil?
|
24
|
+
elements << "tombstone"
|
25
|
+
else
|
26
|
+
value = v[:value]
|
27
|
+
elements << value_of(value).to_s.slice(0..34).ljust(35)
|
28
|
+
elements << value_of(value).class.to_s.slice(0..6).ljust(7)
|
29
|
+
elements << "Match: #{v[:match]}".slice(0..29).ljust(30)
|
30
|
+
elements << "Source: #{v[:source]}"
|
31
|
+
end
|
32
|
+
str += elements.join(" | ") << "\n"
|
20
33
|
end
|
21
34
|
end
|
22
35
|
str
|
23
36
|
end
|
24
37
|
|
25
38
|
def get(property)
|
26
|
-
config =
|
27
|
-
@local_store[property]
|
28
|
-
end
|
39
|
+
config = _get(property)
|
29
40
|
config ? value_of(config[:value]) : nil
|
30
41
|
end
|
31
42
|
|
32
|
-
def
|
33
|
-
|
43
|
+
def get_config(property)
|
44
|
+
config = _get(property)
|
45
|
+
config ? config[:config] : nil
|
34
46
|
end
|
35
47
|
|
36
|
-
def
|
37
|
-
@
|
48
|
+
def _get(key)
|
49
|
+
@lock.with_read_lock do
|
50
|
+
@local_store[key]
|
51
|
+
end
|
38
52
|
end
|
39
53
|
|
40
|
-
|
41
|
-
|
42
|
-
def value_of(config_value)
|
43
|
-
case config_value.type
|
44
|
-
when :string
|
45
|
-
config_value.string
|
46
|
-
when :int
|
47
|
-
config_value.int
|
48
|
-
when :double
|
49
|
-
config_value.double
|
50
|
-
when :bool
|
51
|
-
config_value.bool
|
52
|
-
when :feature_flag
|
53
|
-
config_value.feature_flag
|
54
|
-
end
|
54
|
+
def update
|
55
|
+
make_local
|
55
56
|
end
|
56
57
|
|
58
|
+
private
|
59
|
+
|
57
60
|
# Should client a.b.c see key in namespace a.b? yes
|
58
61
|
# Should client a.b.c see key in namespace a.b.c? yes
|
59
62
|
# Should client a.b.c see key in namespace a.b.d? no
|
@@ -61,33 +64,37 @@ module Prefab
|
|
61
64
|
#
|
62
65
|
def starts_with_ns?(key_namespace, client_namespace)
|
63
66
|
zipped = key_namespace.split(NAMESPACE_DELIMITER).zip(client_namespace.split(NAMESPACE_DELIMITER))
|
64
|
-
zipped.map do |k, c|
|
65
|
-
(k.nil? || k.empty?) ||
|
66
|
-
end
|
67
|
+
mapped = zipped.map do |k, c|
|
68
|
+
(k.nil? || k.empty?) || k == c
|
69
|
+
end
|
70
|
+
[mapped.all?, mapped.size]
|
67
71
|
end
|
68
72
|
|
69
73
|
def make_local
|
70
74
|
store = {}
|
71
|
-
@config_loader.calc_config.each do |
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
store[property] = { namespace: key_namespace, value: value }
|
75
|
+
@config_loader.calc_config.each do |key, config_resolver_obj|
|
76
|
+
config = config_resolver_obj[:config]
|
77
|
+
sortable = config.rows.map do |row|
|
78
|
+
if row.project_env_id != 0
|
79
|
+
if row.project_env_id == @project_env_id
|
80
|
+
if !row.namespace.empty?
|
81
|
+
(starts_with, count) = starts_with_ns?(row.namespace, @namespace)
|
82
|
+
# rubocop:disable BlockNesting
|
83
|
+
{ sortable: 2 + count, match: "nm:#{row.namespace}", value: row.value, config: config} if starts_with
|
84
|
+
else
|
85
|
+
{ sortable: 1, match: "env:#{row.project_env_id}", value: row.value, config: config}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
else
|
89
|
+
match = config_resolver_obj[:match] || "default"
|
90
|
+
{ sortable: 0, match: match, value: row.value, config: config}
|
88
91
|
end
|
89
|
-
end
|
92
|
+
end.compact
|
93
|
+
to_store = sortable.sort_by { |h| h[:sortable] }.last
|
94
|
+
to_store[:source] = config_resolver_obj[:source]
|
95
|
+
store[key] = to_store
|
90
96
|
end
|
97
|
+
|
91
98
|
@lock.with_write_lock do
|
92
99
|
@local_store = store
|
93
100
|
end
|
data/lib/prefab/error.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
module Errors
|
5
|
+
class InitializationTimeoutError < Prefab::Error
|
6
|
+
def initialize(timeout_sec, key)
|
7
|
+
message = "Prefab couldn't initialize in #{timeout_sec} second timeout. Trying to fetch key `#{key}`."
|
8
|
+
|
9
|
+
super(message)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|