prefab-cloud-ruby 0.13.1 → 0.13.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +39 -0
  3. data/.tool-versions +1 -0
  4. data/CODEOWNERS +1 -0
  5. data/Gemfile +11 -4
  6. data/Gemfile.lock +52 -32
  7. data/README.md +19 -1
  8. data/Rakefile +1 -0
  9. data/VERSION +1 -1
  10. data/compile_protos.sh +3 -0
  11. data/lib/prefab/auth_interceptor.rb +1 -0
  12. data/lib/prefab/cancellable_interceptor.rb +1 -0
  13. data/lib/prefab/client.rb +51 -38
  14. data/lib/prefab/config_client.rb +145 -73
  15. data/lib/prefab/config_helper.rb +29 -0
  16. data/lib/prefab/config_loader.rb +98 -13
  17. data/lib/prefab/config_resolver.rb +56 -49
  18. data/lib/prefab/error.rb +6 -0
  19. data/lib/prefab/errors/initialization_timeout_error.rb +13 -0
  20. data/lib/prefab/errors/invalid_api_key_error.rb +19 -0
  21. data/lib/prefab/errors/missing_default_error.rb +13 -0
  22. data/lib/prefab/feature_flag_client.rb +129 -11
  23. data/lib/prefab/internal_logger.rb +29 -0
  24. data/lib/prefab/logger_client.rb +10 -8
  25. data/lib/prefab/murmer3.rb +1 -0
  26. data/lib/prefab/noop_cache.rb +1 -0
  27. data/lib/prefab/noop_stats.rb +1 -0
  28. data/lib/prefab/options.rb +82 -0
  29. data/lib/prefab/ratelimit_client.rb +1 -0
  30. data/lib/prefab-cloud-ruby.rb +10 -0
  31. data/lib/prefab_pb.rb +214 -132
  32. data/lib/prefab_services_pb.rb +35 -6
  33. data/prefab-cloud-ruby.gemspec +29 -10
  34. data/run_test_harness_server.sh +8 -0
  35. data/test/.prefab.test.config.yaml +27 -1
  36. data/test/harness_server.rb +64 -0
  37. data/test/test_client.rb +98 -0
  38. data/test/test_config_client.rb +56 -0
  39. data/test/test_config_loader.rb +39 -25
  40. data/test/test_config_resolver.rb +134 -38
  41. data/test/test_feature_flag_client.rb +277 -35
  42. data/test/test_helper.rb +70 -4
  43. data/test/test_logger.rb +23 -29
  44. metadata +69 -14
  45. data/.ruby-version +0 -1
@@ -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
- @initialization_lock = Concurrent::ReadWriteLock.new
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
- def start_streaming
27
- @streaming = true
28
- start_api_connection_thread(@config_loader.highwater_mark)
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 get(prop)
32
- @initialization_lock.with_read_lock do
33
- @config_resolver.get(prop)
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::ConfigDelta.new(key: [namespace, key].compact.join(":"),
62
- value: config_value)
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
- # Bootstrap out of the cache
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 = load_checkpoint_from_config
124
+ success = load_checkpoint_api_cdn
78
125
 
79
- if !success
80
- @base_client.log_internal Logger::INFO, "Fallback to S3"
81
- load_checkpoint_from_s3
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
- rescue => e
85
- @base_client.log_internal Logger::WARN, "Unexpected problem loading checkpoint #{e}"
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 load_checkpoint_from_config
89
- config_req = Prefab::ConfigServicePointer.new(account_id: @base_client.account_id,
90
- start_at_id: @config_loader.highwater_mark)
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
- load_deltas(resp, :api)
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
- resp = Faraday.get url
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
- deltas = Prefab::ConfigDeltas.decode(resp.body)
109
- load_deltas(deltas, :s3)
183
+ configs = Prefab::Configs.decode(resp.body)
184
+ load_configs(configs, source)
185
+ true
110
186
  else
111
- @base_client.log_internal Logger::INFO, "No S3 checkpoint. Response #{resp.status} Plan may not support this."
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
- def load_deltas(deltas, source)
117
- deltas.deltas.each do |delta|
118
- @config_loader.set(delta)
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::DEBUG, "Unlocked Config via #{source}"
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
- # Setup a streaming connection to the API
154
- # Save new config values into the loader
155
- def start_api_connection_thread(start_at_id)
156
- config_req = Prefab::ConfigServicePointer.new(account_id: @base_client.account_id,
157
- start_at_id: start_at_id)
158
- @base_client.log_internal Logger::DEBUG, "start api connection thread #{start_at_id}"
159
- @base_client.stats.increment("prefab.config.api.start")
160
-
161
- @api_connection_thread = Thread.new do
162
- at_exit do
163
- @streaming = false
164
- @cancellable_interceptor.cancel
165
- end
166
-
167
- while @streaming do
168
- begin
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
@@ -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].value
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(delta)
26
+ def set(config, source)
24
27
  # don't overwrite newer values
25
- if @api_config[delta.key] && @api_config[delta.key].id > delta.id
28
+ if @api_config[config.key] && @api_config[config.key][:config].id >= config.id
26
29
  return
27
30
  end
28
31
 
29
- if delta.value.nil?
30
- @api_config.delete(delta.key)
32
+ if config.rows.empty?
33
+ @api_config.delete(config.key)
31
34
  else
32
- @api_config[delta.key] = delta
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 = [delta.id, @highwater_mark].max
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
- deltas = Prefab::ConfigDeltas.new
48
+ configs = Prefab::Configs.new
43
49
  @api_config.each_value do |config_value|
44
- deltas.deltas << config_value
50
+ configs.configs << config_value[:config]
45
51
  end
46
- deltas
52
+ configs
47
53
  end
48
54
 
49
55
  private
50
56
 
51
57
  def load_classpath_config
52
- classpath_dir = ENV['PREFAB_CONFIG_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 = ENV['PREFAB_CONFIG_OVERRIDE_DIR'] || Dir.home
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
- rtn[k] = Prefab::ConfigValue.new(value_from(v))
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
- NAMESPACE_DELIMITER = ".".freeze
4
- NAME_KEY_DELIMITER = ":".freeze
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
- value = v[:value]
19
- str << "|#{k}| in #{v[:namespace]} |#{value_of(value)}|#{value_of(value).class}\n"
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 = @lock.with_read_lock do
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 update
33
- make_local
43
+ def get_config(property)
44
+ config = _get(property)
45
+ config ? config[:config] : nil
34
46
  end
35
47
 
36
- def export_api_deltas
37
- @config_loader.get_api_deltas
48
+ def _get(key)
49
+ @lock.with_read_lock do
50
+ @local_store[key]
51
+ end
38
52
  end
39
53
 
40
- private
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?) || c == k
66
- end.all?
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 |prop, value|
72
- property = prop
73
- key_namespace = ""
74
-
75
- split = prop.split(NAME_KEY_DELIMITER)
76
-
77
- if split.size > 1
78
- property = split[1..-1].join(NAME_KEY_DELIMITER)
79
- key_namespace = split[0]
80
- end
81
-
82
- if starts_with_ns?(key_namespace, @namespace)
83
- existing = store[property]
84
- if existing.nil?
85
- store[property] = { namespace: key_namespace, value: value }
86
- elsif existing[:namespace].split(NAMESPACE_DELIMITER).size < key_namespace.split(NAMESPACE_DELIMITER).size
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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ class Error < StandardError
5
+ end
6
+ end
@@ -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