prefab-cloud-ruby 0.20.0 → 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.envrc.sample +3 -0
- data/.github/workflows/ruby.yml +4 -0
- data/.gitmodules +3 -0
- data/Gemfile +12 -12
- data/Gemfile.lock +16 -14
- data/README.md +1 -1
- data/Rakefile +13 -14
- data/VERSION +1 -1
- data/lib/prefab/auth_interceptor.rb +2 -1
- data/lib/prefab/cancellable_interceptor.rb +8 -7
- data/lib/prefab/client.rb +33 -24
- data/lib/prefab/config_client.rb +55 -66
- data/lib/prefab/config_loader.rb +7 -114
- data/lib/prefab/config_resolver.rb +27 -57
- data/lib/prefab/config_value_unwrapper.rb +23 -0
- data/lib/prefab/criteria_evaluator.rb +96 -0
- data/lib/prefab/errors/invalid_api_key_error.rb +1 -1
- data/lib/prefab/feature_flag_client.rb +13 -145
- data/lib/prefab/internal_logger.rb +6 -5
- data/lib/prefab/local_config_parser.rb +110 -0
- data/lib/prefab/logger_client.rb +26 -31
- data/lib/prefab/murmer3.rb +3 -4
- data/lib/prefab/noop_cache.rb +5 -7
- data/lib/prefab/noop_stats.rb +2 -3
- data/lib/prefab/options.rb +11 -9
- data/lib/prefab/ratelimit_client.rb +11 -13
- data/lib/prefab/sse_logger.rb +3 -2
- data/lib/prefab/weighted_value_resolver.rb +42 -0
- data/lib/prefab/yaml_config_parser.rb +32 -0
- data/lib/prefab-cloud-ruby.rb +7 -2
- data/lib/prefab_pb.rb +49 -43
- data/lib/prefab_services_pb.rb +0 -1
- data/prefab-cloud-ruby.gemspec +28 -19
- data/test/.prefab.unit_tests.config.yaml +3 -2
- data/test/integration_test.rb +98 -0
- data/test/integration_test_helpers.rb +37 -0
- data/test/test_client.rb +32 -31
- data/test/test_config_client.rb +21 -20
- data/test/test_config_loader.rb +48 -37
- data/test/test_config_resolver.rb +312 -135
- data/test/test_config_value_unwrapper.rb +83 -0
- data/test/test_criteria_evaluator.rb +533 -0
- data/test/test_feature_flag_client.rb +35 -347
- data/test/test_helper.rb +18 -14
- data/test/test_integration.rb +33 -0
- data/test/test_local_config_parser.rb +78 -0
- data/test/test_logger.rb +47 -46
- data/test/test_weighted_value_resolver.rb +65 -0
- metadata +24 -27
- data/lib/prefab/config_helper.rb +0 -31
- data/run_test_harness_server.sh +0 -8
- data/test/harness_server.rb +0 -64
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class LocalConfigParser
|
5
|
+
class << self
|
6
|
+
def parse(key, value, config, file)
|
7
|
+
if value.instance_of?(Hash)
|
8
|
+
if value['feature_flag']
|
9
|
+
config[key] = feature_flag_config(file, key, value)
|
10
|
+
else
|
11
|
+
value.each do |nest_key, nest_value|
|
12
|
+
nested_key = "#{key}.#{nest_key}"
|
13
|
+
nested_key = key if nest_key == '_'
|
14
|
+
parse(nested_key, nest_value, config, file)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
else
|
18
|
+
config[key] = {
|
19
|
+
source: file,
|
20
|
+
match: 'default',
|
21
|
+
config: Prefab::Config.new(
|
22
|
+
config_type: :CONFIG,
|
23
|
+
key: key,
|
24
|
+
rows: [
|
25
|
+
Prefab::ConfigRow.new(values: [
|
26
|
+
Prefab::ConditionalValue.new(value: value_from(key, value))
|
27
|
+
])
|
28
|
+
]
|
29
|
+
)
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
config
|
34
|
+
end
|
35
|
+
|
36
|
+
def value_from(key, raw)
|
37
|
+
case raw
|
38
|
+
when String
|
39
|
+
if key.to_s.start_with? Prefab::LoggerClient::BASE_KEY
|
40
|
+
prefab_log_level_resolve = Prefab::LogLevel.resolve(raw.upcase.to_sym) || Prefab::LogLevel::NOT_SET_LOG_LEVEL
|
41
|
+
{ log_level: prefab_log_level_resolve }
|
42
|
+
else
|
43
|
+
{ string: raw }
|
44
|
+
end
|
45
|
+
when Integer
|
46
|
+
{ int: raw }
|
47
|
+
when TrueClass, FalseClass
|
48
|
+
{ bool: raw }
|
49
|
+
when Float
|
50
|
+
{ double: raw }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def feature_flag_config(file, key, value)
|
55
|
+
criterion = (parse_criterion(value['criterion']) if value['criterion'])
|
56
|
+
|
57
|
+
variant = Prefab::ConfigValue.new(value_from(key, value['value']))
|
58
|
+
|
59
|
+
row = Prefab::ConfigRow.new(
|
60
|
+
values: [
|
61
|
+
Prefab::ConditionalValue.new(
|
62
|
+
criteria: [criterion].compact,
|
63
|
+
value: Prefab::ConfigValue.new(
|
64
|
+
weighted_values: Prefab::WeightedValues.new(weighted_values: [
|
65
|
+
Prefab::WeightedValue.new(
|
66
|
+
weight: 1000,
|
67
|
+
value: variant
|
68
|
+
)
|
69
|
+
])
|
70
|
+
)
|
71
|
+
)
|
72
|
+
]
|
73
|
+
)
|
74
|
+
|
75
|
+
raise Prefab::Error, "Feature flag config `#{key}` #{file} must have a `value`" unless value.key?('value')
|
76
|
+
|
77
|
+
{
|
78
|
+
source: file,
|
79
|
+
match: key,
|
80
|
+
config: Prefab::Config.new(
|
81
|
+
config_type: :FEATURE_FLAG,
|
82
|
+
key: key,
|
83
|
+
allowable_values: [variant],
|
84
|
+
rows: [row]
|
85
|
+
)
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
def parse_criterion(criterion)
|
90
|
+
Prefab::Criterion.new(operator: criterion['operator'],
|
91
|
+
property_name: parse_property(criterion),
|
92
|
+
value_to_match: parse_value_to_match(criterion['values']))
|
93
|
+
end
|
94
|
+
|
95
|
+
def parse_property(criterion)
|
96
|
+
if criterion['operator'] == 'LOOKUP_KEY_IN'
|
97
|
+
Prefab::CriteriaEvaluator::LOOKUP_KEY
|
98
|
+
else
|
99
|
+
criterion['property']
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def parse_value_to_match(values)
|
104
|
+
raise "Can't handle #{values}" unless values.instance_of?(Array)
|
105
|
+
|
106
|
+
Prefab::ConfigValue.new(string_list: Prefab::StringList.new(values: values))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/prefab/logger_client.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Prefab
|
3
4
|
class LoggerClient < Logger
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
INTERNAL_PREFIX = "cloud.prefab.client"
|
5
|
+
SEP = '.'
|
6
|
+
BASE_KEY = 'log-level'
|
7
|
+
UNKNOWN_PATH = 'unknown.'
|
8
|
+
INTERNAL_PREFIX = 'cloud.prefab.client'
|
9
9
|
|
10
10
|
LOG_LEVEL_LOOKUPS = {
|
11
11
|
Prefab::LogLevel::NOT_SET_LOG_LEVEL => Logger::DEBUG,
|
@@ -33,20 +33,18 @@ module Prefab
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def log_internal(message, path = nil, progname, severity, &block)
|
36
|
-
if path
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
path = if path
|
37
|
+
"#{INTERNAL_PREFIX}.#{path}"
|
38
|
+
else
|
39
|
+
INTERNAL_PREFIX
|
40
|
+
end
|
41
41
|
|
42
42
|
log(message, path, progname, severity, &block)
|
43
43
|
end
|
44
44
|
|
45
|
-
def log(message, path, progname, severity
|
45
|
+
def log(message, path, progname, severity)
|
46
46
|
severity ||= Logger::UNKNOWN
|
47
|
-
if @logdev.nil? || severity < level_of(path) || @silences[local_log_id]
|
48
|
-
return true
|
49
|
-
end
|
47
|
+
return true if @logdev.nil? || severity < level_of(path) || @silences[local_log_id]
|
50
48
|
|
51
49
|
progname = "#{path}: #{progname || @progname}"
|
52
50
|
|
@@ -60,7 +58,8 @@ module Prefab
|
|
60
58
|
end
|
61
59
|
|
62
60
|
@logdev.write(
|
63
|
-
format_message(format_severity(severity), Time.now, progname, message)
|
61
|
+
format_message(format_severity(severity), Time.now, progname, message)
|
62
|
+
)
|
64
63
|
true
|
65
64
|
end
|
66
65
|
|
@@ -85,23 +84,23 @@ module Prefab
|
|
85
84
|
end
|
86
85
|
|
87
86
|
def debug?
|
88
|
-
true
|
87
|
+
true
|
89
88
|
end
|
90
89
|
|
91
90
|
def info?
|
92
|
-
true
|
91
|
+
true
|
93
92
|
end
|
94
93
|
|
95
94
|
def warn?
|
96
|
-
true
|
95
|
+
true
|
97
96
|
end
|
98
97
|
|
99
98
|
def error?
|
100
|
-
true
|
99
|
+
true
|
101
100
|
end
|
102
101
|
|
103
102
|
def fatal?
|
104
|
-
true
|
103
|
+
true
|
105
104
|
end
|
106
105
|
|
107
106
|
def level
|
@@ -128,13 +127,10 @@ module Prefab
|
|
128
127
|
# Find the closest match to 'log_level.path' in config
|
129
128
|
def level_of(path)
|
130
129
|
closest_log_level_match = @config_client.get(BASE_KEY, :WARN)
|
131
|
-
path.split(SEP).
|
130
|
+
path.split(SEP).each_with_object([BASE_KEY]) do |n, memo|
|
132
131
|
memo << n
|
133
132
|
val = @config_client.get(memo.join(SEP), nil)
|
134
|
-
unless val.nil?
|
135
|
-
closest_log_level_match = val
|
136
|
-
end
|
137
|
-
memo
|
133
|
+
closest_log_level_match = val unless val.nil?
|
138
134
|
end
|
139
135
|
closest_log_level_match_int = Prefab::LogLevel.resolve(closest_log_level_match)
|
140
136
|
LOG_LEVEL_LOOKUPS[closest_log_level_match_int]
|
@@ -150,10 +146,10 @@ module Prefab
|
|
150
146
|
def get_path(absolute_path, base_label)
|
151
147
|
path = (absolute_path || UNKNOWN_PATH).dup
|
152
148
|
path.slice! Dir.pwd
|
153
|
-
path.gsub!(
|
149
|
+
path.gsub!(%r{(.*)?(?=/lib)}im, '') # replace everything before first lib
|
154
150
|
|
155
|
-
path = path.gsub(
|
156
|
-
path.slice!
|
151
|
+
path = path.gsub('/', SEP).gsub(/.rb.*/, '') + SEP + base_label
|
152
|
+
path.slice! '.lib'
|
157
153
|
path.slice! SEP
|
158
154
|
path
|
159
155
|
end
|
@@ -162,9 +158,8 @@ module Prefab
|
|
162
158
|
# StubConfigClient to be used while config client initializes
|
163
159
|
# since it may log
|
164
160
|
class BootstrappingConfigClient
|
165
|
-
def get(
|
166
|
-
ENV[
|
161
|
+
def get(_key, default = nil)
|
162
|
+
ENV['PREFAB_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'] ? ENV['PREFAB_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'].upcase.to_sym : default
|
167
163
|
end
|
168
164
|
end
|
169
165
|
end
|
170
|
-
|
data/lib/prefab/murmer3.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
class Murmur3
|
3
4
|
## MurmurHash3 was written by Austin Appleby, and is placed in the public
|
4
5
|
## domain. The author hereby disclaims copyright to this source code.
|
@@ -9,7 +10,6 @@ class Murmur3
|
|
9
10
|
((x << r) | (x >> (32 - r))) & MASK32
|
10
11
|
end
|
11
12
|
|
12
|
-
|
13
13
|
def self.murmur3_32_fmix(h)
|
14
14
|
h &= MASK32
|
15
15
|
h ^= h >> 16
|
@@ -25,7 +25,7 @@ class Murmur3
|
|
25
25
|
(k1 * 0x1b873593) & MASK32
|
26
26
|
end
|
27
27
|
|
28
|
-
def self.murmur3_32(str, seed=0)
|
28
|
+
def self.murmur3_32(str, seed = 0)
|
29
29
|
h1 = seed
|
30
30
|
numbers = str.unpack('V*C*')
|
31
31
|
tailn = str.length % 4
|
@@ -33,7 +33,7 @@ class Murmur3
|
|
33
33
|
for k1 in numbers
|
34
34
|
h1 ^= murmur3_32__mmix(k1)
|
35
35
|
h1 = murmur3_32_rotl(h1, 13)
|
36
|
-
h1 = (h1*5 + 0xe6546b64) & MASK32
|
36
|
+
h1 = (h1 * 5 + 0xe6546b64) & MASK32
|
37
37
|
end
|
38
38
|
|
39
39
|
unless tail.empty?
|
@@ -47,5 +47,4 @@ class Murmur3
|
|
47
47
|
h1 ^= str.length
|
48
48
|
murmur3_32_fmix(h1)
|
49
49
|
end
|
50
|
-
|
51
50
|
end
|
data/lib/prefab/noop_cache.rb
CHANGED
@@ -1,17 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Prefab
|
3
4
|
class NoopCache
|
4
|
-
def fetch(
|
5
|
+
def fetch(_name, _opts)
|
5
6
|
yield
|
6
7
|
end
|
7
8
|
|
8
|
-
def write(name, value, opts=nil)
|
9
|
-
end
|
9
|
+
def write(name, value, opts = nil); end
|
10
10
|
|
11
|
-
def read(name)
|
12
|
-
end
|
11
|
+
def read(name); end
|
13
12
|
|
14
|
-
def delete(name)
|
15
|
-
end
|
13
|
+
def delete(name); end
|
16
14
|
end
|
17
15
|
end
|
data/lib/prefab/noop_stats.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
module Prefab
|
3
2
|
|
3
|
+
module Prefab
|
4
4
|
class NoopStats
|
5
5
|
# receives increment("prefab.ratelimit.limitcheck", {:tags=>["policy_group:page_view", "pass:true"]})
|
6
|
-
def increment(name, opts={})
|
7
|
-
end
|
6
|
+
def increment(name, opts = {}); end
|
8
7
|
end
|
9
8
|
end
|
data/lib/prefab/options.rb
CHANGED
@@ -19,17 +19,19 @@ module Prefab
|
|
19
19
|
attr_reader :prefab_envs
|
20
20
|
|
21
21
|
DEFAULT_LOG_FORMATTER = proc { |severity, datetime, progname, msg|
|
22
|
-
"#{severity.ljust(5)} #{datetime}:#{
|
22
|
+
"#{severity.ljust(5)} #{datetime}:#{' ' if progname}#{progname} #{msg}\n"
|
23
23
|
}
|
24
24
|
|
25
25
|
module ON_INITIALIZATION_FAILURE
|
26
26
|
RAISE = 1
|
27
27
|
RETURN = 2
|
28
28
|
end
|
29
|
+
|
29
30
|
module ON_NO_DEFAULT
|
30
31
|
RAISE = 1
|
31
32
|
RETURN_NIL = 2
|
32
33
|
end
|
34
|
+
|
33
35
|
module DATASOURCES
|
34
36
|
ALL = 1
|
35
37
|
LOCAL_ONLY = 2
|
@@ -40,20 +42,20 @@ module Prefab
|
|
40
42
|
logdev: $stdout,
|
41
43
|
stats: NoopStats.new, # receives increment("prefab.limitcheck", {:tags=>["policy_group:page_view", "pass:true"]})
|
42
44
|
shared_cache: NoopCache.new, # Something that quacks like Rails.cache ideally memcached
|
43
|
-
namespace:
|
45
|
+
namespace: '',
|
44
46
|
log_formatter: DEFAULT_LOG_FORMATTER,
|
45
47
|
log_prefix: nil,
|
46
|
-
prefab_api_url: ENV[
|
47
|
-
prefab_grpc_url: ENV[
|
48
|
+
prefab_api_url: ENV['PREFAB_API_URL'] || 'https://api.prefab.cloud',
|
49
|
+
prefab_grpc_url: ENV['PREFAB_GRPC_URL'] || 'grpc.prefab.cloud:443',
|
48
50
|
on_no_default: ON_NO_DEFAULT::RAISE, # options :raise, :warn_and_return_nil,
|
49
51
|
initialization_timeout_sec: 10, # how long to wait before on_init_failure
|
50
|
-
on_init_failure: ON_INITIALIZATION_FAILURE::RAISE, #options :unlock_and_continue, :lock_and_keep_trying, :raise
|
52
|
+
on_init_failure: ON_INITIALIZATION_FAILURE::RAISE, # options :unlock_and_continue, :lock_and_keep_trying, :raise
|
51
53
|
# new_config_callback: nil, #callback method
|
52
54
|
# live_override_url: nil,
|
53
|
-
prefab_datasources: ENV['PREFAB_DATASOURCES'] ==
|
55
|
+
prefab_datasources: ENV['PREFAB_DATASOURCES'] == 'LOCAL_ONLY' ? DATASOURCES::LOCAL_ONLY : DATASOURCES::ALL,
|
54
56
|
prefab_config_override_dir: Dir.home,
|
55
|
-
prefab_config_classpath_dir:
|
56
|
-
prefab_envs:
|
57
|
+
prefab_config_classpath_dir: '.',
|
58
|
+
prefab_envs: ENV['PREFAB_ENVS'].nil? ? [] : ENV['PREFAB_ENVS'].split(',')
|
57
59
|
)
|
58
60
|
# debugger
|
59
61
|
@api_key = api_key
|
@@ -80,7 +82,7 @@ module Prefab
|
|
80
82
|
|
81
83
|
# https://api.prefab.cloud -> https://api-prefab-cloud.global.ssl.fastly.net
|
82
84
|
def url_for_api_cdn
|
83
|
-
ENV['PREFAB_CDN_URL'] || "#{@prefab_api_url.gsub(/\./,
|
85
|
+
ENV['PREFAB_CDN_URL'] || "#{@prefab_api_url.gsub(/\./, '-')}.global.ssl.fastly.net"
|
84
86
|
end
|
85
87
|
end
|
86
88
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Prefab
|
3
4
|
class RateLimitClient
|
4
|
-
|
5
5
|
def initialize(base_client, timeout)
|
6
6
|
@timeout = timeout
|
7
7
|
@base_client = base_client
|
@@ -9,14 +9,14 @@ module Prefab
|
|
9
9
|
|
10
10
|
def pass?(group)
|
11
11
|
result = acquire([group], 1)
|
12
|
-
|
12
|
+
result.passed
|
13
13
|
end
|
14
14
|
|
15
15
|
def acquire(groups, acquire_amount, allow_partial_response: false, on_error: :log_and_pass)
|
16
|
-
expiry_cache_key = "prefab.ratelimit.expiry:#{groups.join(
|
16
|
+
expiry_cache_key = "prefab.ratelimit.expiry:#{groups.join('.')}"
|
17
17
|
expiry = @base_client.shared_cache.read(expiry_cache_key)
|
18
18
|
if !expiry.nil? && Integer(expiry) > Time.now.utc.to_f * 1000
|
19
|
-
@base_client.stats.increment(
|
19
|
+
@base_client.stats.increment('prefab.ratelimit.limitcheck.expirycache.hit', tags: [])
|
20
20
|
return Prefab::LimitResponse.new(passed: false, amount: 0)
|
21
21
|
end
|
22
22
|
|
@@ -27,16 +27,17 @@ module Prefab
|
|
27
27
|
allow_partial_response: allow_partial_response
|
28
28
|
)
|
29
29
|
|
30
|
-
result = @base_client.request Prefab::RateLimitService, :limit_check, req_options: {timeout: @timeout},
|
30
|
+
result = @base_client.request Prefab::RateLimitService, :limit_check, req_options: { timeout: @timeout },
|
31
|
+
params: req
|
31
32
|
|
32
33
|
reset = result.limit_reset_at
|
33
34
|
@base_client.shared_cache.write(expiry_cache_key, reset) unless reset < 1 # protobuf default int to 0
|
34
35
|
|
35
|
-
@base_client.stats.increment(
|
36
|
+
@base_client.stats.increment('prefab.ratelimit.limitcheck',
|
37
|
+
tags: ["policy_group:#{result.policy_group}", "pass:#{result.passed}"])
|
36
38
|
|
37
39
|
result
|
38
|
-
|
39
|
-
rescue => e
|
40
|
+
rescue StandardError => e
|
40
41
|
handle_error(e, on_error, groups)
|
41
42
|
end
|
42
43
|
|
@@ -48,9 +49,7 @@ module Prefab
|
|
48
49
|
limit: limit,
|
49
50
|
burst: burst
|
50
51
|
)
|
51
|
-
unless safety_level.nil?
|
52
|
-
limit_definition.safety_level = safety_level
|
53
|
-
end
|
52
|
+
limit_definition.safety_level = safety_level unless safety_level.nil?
|
54
53
|
config_value = Prefab::ConfigValue.new(limit_definition: limit_definition)
|
55
54
|
config_delta = Prefab::ConfigClient.value_to_delta(key, config_value)
|
56
55
|
upsert_req = Prefab::UpsertRequest.new(config_delta: config_delta)
|
@@ -61,7 +60,7 @@ module Prefab
|
|
61
60
|
private
|
62
61
|
|
63
62
|
def handle_error(e, on_error, groups)
|
64
|
-
@base_client.stats.increment(
|
63
|
+
@base_client.stats.increment('prefab.ratelimit.error', tags: ['type:limit'])
|
65
64
|
|
66
65
|
message = "ratelimit for #{groups} error: #{e.message}"
|
67
66
|
case on_error
|
@@ -77,4 +76,3 @@ module Prefab
|
|
77
76
|
end
|
78
77
|
end
|
79
78
|
end
|
80
|
-
|
data/lib/prefab/sse_logger.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Prefab
|
3
4
|
class SseLogger < InternalLogger
|
4
5
|
def initialize(logger)
|
5
|
-
super(
|
6
|
+
super('sse', logger)
|
6
7
|
end
|
7
8
|
|
8
9
|
# The SSE::Client warns on a perfectly normal stream disconnect, recast to info
|
9
|
-
def warn(progname = nil
|
10
|
+
def warn(progname = nil)
|
10
11
|
@logger.log_internal yield, @path, progname, INFO
|
11
12
|
end
|
12
13
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class WeightedValueResolver
|
5
|
+
MAX_32_FLOAT = 4_294_967_294.0
|
6
|
+
|
7
|
+
def initialize(weights, config_key, lookup_key)
|
8
|
+
@weights = weights
|
9
|
+
@config_key = config_key
|
10
|
+
@lookup_key = lookup_key
|
11
|
+
end
|
12
|
+
|
13
|
+
def resolve
|
14
|
+
percent = @lookup_key ? user_percent : rand
|
15
|
+
|
16
|
+
index = variant_index(percent)
|
17
|
+
|
18
|
+
@weights[index]
|
19
|
+
end
|
20
|
+
|
21
|
+
def user_percent
|
22
|
+
to_hash = "#{@config_key}#{@lookup_key}"
|
23
|
+
int_value = Murmur3.murmur3_32(to_hash)
|
24
|
+
int_value / MAX_32_FLOAT
|
25
|
+
end
|
26
|
+
|
27
|
+
def variant_index(percent_through_distribution)
|
28
|
+
distribution_space = @weights.inject(0) { |sum, v| sum + v.weight }
|
29
|
+
bucket = distribution_space * percent_through_distribution
|
30
|
+
|
31
|
+
sum = 0
|
32
|
+
@weights.each_with_index do |variant_weight, index|
|
33
|
+
return index if bucket < sum + variant_weight.weight
|
34
|
+
|
35
|
+
sum += variant_weight.weight
|
36
|
+
end
|
37
|
+
|
38
|
+
# In the event that all weights are zero, return the last variant
|
39
|
+
@weights.size - 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class YAMLConfigParser
|
5
|
+
def initialize(file, client)
|
6
|
+
@file = file
|
7
|
+
@client = client
|
8
|
+
end
|
9
|
+
|
10
|
+
def merge(config)
|
11
|
+
yaml = load
|
12
|
+
|
13
|
+
yaml.each do |k, v|
|
14
|
+
config = Prefab::LocalConfigParser.parse(k, v, config, @file)
|
15
|
+
end
|
16
|
+
|
17
|
+
config
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def load
|
23
|
+
if File.exist?(@file)
|
24
|
+
@client.log_internal Logger::INFO, "Load #{@file}"
|
25
|
+
YAML.load_file(@file)
|
26
|
+
else
|
27
|
+
@client.log_internal Logger::INFO, "No file #{@file}"
|
28
|
+
{}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/prefab-cloud-ruby.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
|
3
|
+
require 'concurrent/atomics'
|
3
4
|
require 'concurrent'
|
4
5
|
require 'faraday'
|
5
6
|
require 'openssl'
|
@@ -14,8 +15,12 @@ require 'prefab_services_pb'
|
|
14
15
|
require 'prefab/options'
|
15
16
|
require 'prefab/internal_logger'
|
16
17
|
require 'prefab/sse_logger'
|
17
|
-
require 'prefab/
|
18
|
+
require 'prefab/weighted_value_resolver'
|
19
|
+
require 'prefab/config_value_unwrapper'
|
20
|
+
require 'prefab/criteria_evaluator'
|
18
21
|
require 'prefab/config_loader'
|
22
|
+
require 'prefab/local_config_parser'
|
23
|
+
require 'prefab/yaml_config_parser'
|
19
24
|
require 'prefab/config_resolver'
|
20
25
|
require 'prefab/client'
|
21
26
|
require 'prefab/ratelimit_client'
|