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