prefab-cloud-ruby 0.5.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.tool-versions +1 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +6 -3
- data/Gemfile.lock +63 -47
- data/README.md +7 -1
- data/VERSION +1 -1
- data/compile_protos.sh +3 -0
- data/lib/prefab/client.rb +7 -4
- data/lib/prefab/config_client.rb +11 -5
- data/lib/prefab/config_helper.rb +20 -0
- data/lib/prefab/config_loader.rb +3 -3
- data/lib/prefab/config_resolver.rb +38 -34
- data/lib/prefab/feature_flag_client.rb +99 -11
- data/lib/prefab-cloud-ruby.rb +1 -0
- data/lib/prefab_pb.rb +206 -132
- data/lib/prefab_services_pb.rb +6 -6
- data/prefab-cloud-ruby.gemspec +30 -30
- data/run_test_harness_server.sh +2 -0
- data/test/harness_server.rb +46 -0
- data/test/test_config_loader.rb +12 -12
- data/test/test_config_resolver.rb +107 -37
- data/test/test_feature_flag_client.rb +191 -35
- data/test/test_helper.rb +13 -3
- metadata +70 -25
- data/.ruby-version +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 171f3c104141000ec55b8c331f9f0439d56e991936c69a032f7c78394560ddea
|
4
|
+
data.tar.gz: bb2fa93f2bc589c7e58bda95f7ab9519d780d80de212b62e1a29b1e187ab55a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c883ea3ba1f608c5fd88e0371302dca440968ad34fae8c279b735f180a73986498c9a87b95d2d51571daa2be723944568824bf8444425a8ff463c210bb9c7429
|
7
|
+
data.tar.gz: 278cccca1c2e834e79971394310fdba30c03e806b31708094d90d62ed1623c7c5d2e19cdd204165437a75a8711c979f915f0b1ecd86220991fa22697ddad3a49
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 3.0.3
|
data/CODEOWNERS
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* @prefab-cloud/prefabdevs @prefab-cloud/prefabmaintainers @prefab-cloud/prefabadmins
|
data/Gemfile
CHANGED
@@ -2,13 +2,16 @@ source "https://rubygems.org"
|
|
2
2
|
|
3
3
|
gem 'concurrent-ruby', '~> 1.0', '>= 1.0.5'
|
4
4
|
gem 'faraday'
|
5
|
-
gem 'grpc',
|
5
|
+
gem 'grpc', :platforms => :ruby
|
6
|
+
gem 'google-protobuf', :platforms => :ruby
|
7
|
+
gem 'googleapis-common-protos-types', :platforms => :ruby
|
6
8
|
|
7
9
|
group :development do
|
8
|
-
gem 'grpc-tools',
|
10
|
+
gem 'grpc-tools', :platforms => :ruby
|
9
11
|
gem "shoulda", ">= 0"
|
10
12
|
gem "rdoc", "~> 3.12"
|
11
|
-
gem "bundler"
|
13
|
+
gem "bundler"
|
12
14
|
gem "juwelier", "~> 2.4.9"
|
13
15
|
gem "simplecov", ">= 0"
|
16
|
+
gem 'thin'
|
14
17
|
end
|
data/Gemfile.lock
CHANGED
@@ -1,37 +1,43 @@
|
|
1
1
|
GEM
|
2
2
|
remote: https://rubygems.org/
|
3
3
|
specs:
|
4
|
-
activesupport (5.2.
|
4
|
+
activesupport (5.2.4.5)
|
5
5
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
6
6
|
i18n (>= 0.7, < 2)
|
7
7
|
minitest (~> 5.1)
|
8
8
|
tzinfo (~> 1.1)
|
9
|
-
addressable (2.
|
10
|
-
public_suffix (>= 2.0.2, <
|
11
|
-
builder (3.2.
|
12
|
-
concurrent-ruby (1.
|
9
|
+
addressable (2.7.0)
|
10
|
+
public_suffix (>= 2.0.2, < 5.0)
|
11
|
+
builder (3.2.4)
|
12
|
+
concurrent-ruby (1.1.8)
|
13
|
+
daemons (1.4.1)
|
13
14
|
descendants_tracker (0.0.4)
|
14
15
|
thread_safe (~> 0.3, >= 0.3.1)
|
15
|
-
docile (1.3.
|
16
|
-
|
16
|
+
docile (1.3.5)
|
17
|
+
eventmachine (1.2.7)
|
18
|
+
faraday (1.3.0)
|
19
|
+
faraday-net_http (~> 1.0)
|
17
20
|
multipart-post (>= 1.2, < 3)
|
18
|
-
|
19
|
-
|
21
|
+
ruby2_keywords
|
22
|
+
faraday-net_http (1.0.1)
|
23
|
+
git (1.8.1)
|
24
|
+
rchardet (~> 1.8)
|
25
|
+
github_api (0.19.0)
|
20
26
|
addressable (~> 2.4)
|
21
27
|
descendants_tracker (~> 0.0.4)
|
22
|
-
faraday (
|
28
|
+
faraday (>= 0.8, < 2)
|
23
29
|
hashie (~> 3.5, >= 3.5.2)
|
24
30
|
oauth2 (~> 1.0)
|
25
|
-
google-protobuf (3.
|
26
|
-
googleapis-common-protos-types (1.0
|
27
|
-
google-protobuf (~> 3.
|
28
|
-
grpc (1.
|
29
|
-
google-protobuf (~> 3.
|
30
|
-
googleapis-common-protos-types (~> 1.0
|
31
|
-
grpc-tools (1.
|
31
|
+
google-protobuf (3.19.3)
|
32
|
+
googleapis-common-protos-types (1.3.0)
|
33
|
+
google-protobuf (~> 3.14)
|
34
|
+
grpc (1.43.1)
|
35
|
+
google-protobuf (~> 3.18)
|
36
|
+
googleapis-common-protos-types (~> 1.0)
|
37
|
+
grpc-tools (1.43.1)
|
32
38
|
hashie (3.6.0)
|
33
|
-
highline (2.0.
|
34
|
-
i18n (1.
|
39
|
+
highline (2.0.3)
|
40
|
+
i18n (1.8.9)
|
35
41
|
concurrent-ruby (~> 1.0)
|
36
42
|
json (1.8.6)
|
37
43
|
juwelier (2.4.9)
|
@@ -46,57 +52,67 @@ GEM
|
|
46
52
|
rake
|
47
53
|
rdoc
|
48
54
|
semver2
|
49
|
-
jwt (2.
|
55
|
+
jwt (2.2.2)
|
50
56
|
kamelcase (0.0.2)
|
51
57
|
semver2 (~> 3)
|
52
|
-
mini_portile2 (2.
|
53
|
-
minitest (5.
|
54
|
-
multi_json (1.
|
58
|
+
mini_portile2 (2.7.1)
|
59
|
+
minitest (5.14.4)
|
60
|
+
multi_json (1.15.0)
|
55
61
|
multi_xml (0.6.0)
|
56
|
-
multipart-post (2.
|
57
|
-
nokogiri (1.
|
58
|
-
mini_portile2 (~> 2.
|
59
|
-
|
60
|
-
|
62
|
+
multipart-post (2.1.1)
|
63
|
+
nokogiri (1.13.1)
|
64
|
+
mini_portile2 (~> 2.7.0)
|
65
|
+
racc (~> 1.4)
|
66
|
+
oauth2 (1.4.7)
|
67
|
+
faraday (>= 0.8, < 2.0)
|
61
68
|
jwt (>= 1.0, < 3.0)
|
62
69
|
multi_json (~> 1.3)
|
63
70
|
multi_xml (~> 0.5)
|
64
71
|
rack (>= 1.2, < 3)
|
65
|
-
psych (3.1
|
66
|
-
public_suffix (
|
67
|
-
|
68
|
-
|
72
|
+
psych (3.3.1)
|
73
|
+
public_suffix (4.0.6)
|
74
|
+
racc (1.6.0)
|
75
|
+
rack (2.2.3)
|
76
|
+
rake (13.0.3)
|
77
|
+
rchardet (1.8.0)
|
69
78
|
rdoc (3.12.2)
|
70
79
|
json (~> 1.4)
|
80
|
+
ruby2_keywords (0.0.4)
|
71
81
|
semver2 (3.4.2)
|
72
|
-
shoulda (
|
73
|
-
shoulda-context (~>
|
74
|
-
shoulda-matchers (
|
75
|
-
shoulda-context (
|
76
|
-
shoulda-matchers (
|
77
|
-
activesupport (>=
|
78
|
-
simplecov (0.
|
82
|
+
shoulda (4.0.0)
|
83
|
+
shoulda-context (~> 2.0)
|
84
|
+
shoulda-matchers (~> 4.0)
|
85
|
+
shoulda-context (2.0.0)
|
86
|
+
shoulda-matchers (4.5.1)
|
87
|
+
activesupport (>= 4.2.0)
|
88
|
+
simplecov (0.18.5)
|
79
89
|
docile (~> 1.1)
|
80
|
-
|
81
|
-
|
82
|
-
|
90
|
+
simplecov-html (~> 0.11)
|
91
|
+
simplecov-html (0.12.3)
|
92
|
+
thin (1.8.1)
|
93
|
+
daemons (~> 1.0, >= 1.0.9)
|
94
|
+
eventmachine (~> 1.0, >= 1.0.4)
|
95
|
+
rack (>= 1, < 3)
|
83
96
|
thread_safe (0.3.6)
|
84
|
-
tzinfo (1.2.
|
97
|
+
tzinfo (1.2.9)
|
85
98
|
thread_safe (~> 0.1)
|
86
99
|
|
87
100
|
PLATFORMS
|
88
101
|
ruby
|
89
102
|
|
90
103
|
DEPENDENCIES
|
91
|
-
bundler
|
104
|
+
bundler
|
92
105
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
93
106
|
faraday
|
94
|
-
|
95
|
-
|
107
|
+
google-protobuf
|
108
|
+
googleapis-common-protos-types
|
109
|
+
grpc
|
110
|
+
grpc-tools
|
96
111
|
juwelier (~> 2.4.9)
|
97
112
|
rdoc (~> 3.12)
|
98
113
|
shoulda
|
99
114
|
simplecov
|
115
|
+
thin
|
100
116
|
|
101
117
|
BUNDLED WITH
|
102
|
-
|
118
|
+
2.3.5
|
data/README.md
CHANGED
@@ -53,7 +53,13 @@ end
|
|
53
53
|
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
54
54
|
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
55
55
|
|
56
|
+
## Release
|
57
|
+
|
58
|
+
```shell
|
59
|
+
REMOTE_BRANCH=main LOCAL_BRANCH=main bundle exec rake release
|
60
|
+
```
|
61
|
+
|
56
62
|
## Copyright
|
57
63
|
|
58
|
-
Copyright (c)
|
64
|
+
Copyright (c) 2022 Jeff Dwyer. See LICENSE.txt for
|
59
65
|
further details.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.8.0
|
data/compile_protos.sh
CHANGED
data/lib/prefab/client.rb
CHANGED
@@ -8,7 +8,7 @@ module Prefab
|
|
8
8
|
}
|
9
9
|
|
10
10
|
|
11
|
-
attr_reader :
|
11
|
+
attr_reader :project_id, :shared_cache, :stats, :namespace, :interceptor, :api_key, :environment
|
12
12
|
|
13
13
|
def initialize(api_key: ENV['PREFAB_API_KEY'],
|
14
14
|
logdev: nil,
|
@@ -19,13 +19,15 @@ module Prefab
|
|
19
19
|
log_formatter: DEFAULT_LOG_FORMATTER
|
20
20
|
)
|
21
21
|
raise "No API key. Set PREFAB_API_KEY env var" if api_key.nil? || api_key.empty?
|
22
|
+
raise "PREFAB_API_KEY format invalid. Expecting 123-development-yourapikey-SDK" unless api_key.count("-") == 3
|
22
23
|
@logdev = (logdev || $stdout)
|
23
24
|
@log_formatter = log_formatter
|
24
25
|
@local = local
|
25
26
|
@stats = (stats || NoopStats.new)
|
26
27
|
@shared_cache = (shared_cache || NoopCache.new)
|
27
28
|
@api_key = api_key
|
28
|
-
@
|
29
|
+
@project_id = api_key.split("-")[0].to_i
|
30
|
+
@environment = api_key.split("-")[1]
|
29
31
|
@namespace = namespace
|
30
32
|
@interceptor = Prefab::AuthInterceptor.new(api_key)
|
31
33
|
@stubs = {}
|
@@ -37,7 +39,8 @@ module Prefab
|
|
37
39
|
|
38
40
|
def channel
|
39
41
|
credentials = ENV["PREFAB_CLOUD_HTTP"] == "true" ? :this_channel_is_insecure : creds
|
40
|
-
url = ENV["PREFAB_API_URL"] || '
|
42
|
+
url = ENV["PREFAB_API_URL"] || 'grpc.prefab.cloud:443'
|
43
|
+
log_internal Logger::DEBUG, "GRPC Channel #{url} #{credentials}"
|
41
44
|
@_channel ||= GRPC::Core::Channel.new(url, nil, credentials)
|
42
45
|
end
|
43
46
|
|
@@ -88,7 +91,7 @@ module Prefab
|
|
88
91
|
end
|
89
92
|
|
90
93
|
def cache_key(post_fix)
|
91
|
-
"prefab:#{
|
94
|
+
"prefab:#{project_id}:#{post_fix}"
|
92
95
|
end
|
93
96
|
|
94
97
|
def reset!
|
data/lib/prefab/config_client.rb
CHANGED
@@ -6,6 +6,7 @@ module Prefab
|
|
6
6
|
|
7
7
|
def initialize(base_client, timeout)
|
8
8
|
@base_client = base_client
|
9
|
+
@base_client.log_internal Logger::DEBUG, "Initialize ConfigClient"
|
9
10
|
@timeout = timeout
|
10
11
|
@initialization_lock = Concurrent::ReadWriteLock.new
|
11
12
|
|
@@ -14,11 +15,14 @@ module Prefab
|
|
14
15
|
@config_loader = Prefab::ConfigLoader.new(@base_client)
|
15
16
|
@config_resolver = Prefab::ConfigResolver.new(@base_client, @config_loader)
|
16
17
|
|
18
|
+
@base_client.log_internal Logger::DEBUG, "Initialize ConfigClient: AcquireWriteLock"
|
17
19
|
@initialization_lock.acquire_write_lock
|
20
|
+
@base_client.log_internal Logger::DEBUG, "Initialize ConfigClient: AcquiredWriteLock"
|
18
21
|
|
19
22
|
@cancellable_interceptor = Prefab::CancellableInterceptor.new(@base_client)
|
20
23
|
|
21
24
|
@s3_cloud_front = ENV["PREFAB_S3CF_BUCKET"] || DEFAULT_S3CF_BUCKET
|
25
|
+
|
22
26
|
load_checkpoint
|
23
27
|
start_checkpointing_thread
|
24
28
|
end
|
@@ -28,9 +32,9 @@ module Prefab
|
|
28
32
|
start_api_connection_thread(@config_loader.highwater_mark)
|
29
33
|
end
|
30
34
|
|
31
|
-
def get(
|
35
|
+
def get(key)
|
32
36
|
@initialization_lock.with_read_lock do
|
33
|
-
@config_resolver.get(
|
37
|
+
@config_resolver.get(key)
|
34
38
|
end
|
35
39
|
end
|
36
40
|
|
@@ -86,9 +90,12 @@ module Prefab
|
|
86
90
|
end
|
87
91
|
|
88
92
|
def load_checkpoint_from_config
|
89
|
-
|
93
|
+
@base_client.log_internal Logger::DEBUG, "Load Checkpoint From Config"
|
94
|
+
|
95
|
+
config_req = Prefab::ConfigServicePointer.new(project_id: @base_client.project_id,
|
90
96
|
start_at_id: @config_loader.highwater_mark)
|
91
97
|
resp = stub.get_all_config(config_req)
|
98
|
+
@base_client.log_internal Logger::DEBUG, "Got Response #{resp}"
|
92
99
|
load_deltas(resp, :api)
|
93
100
|
resp.deltas.each do |delta|
|
94
101
|
@config_loader.set(delta)
|
@@ -112,7 +119,6 @@ module Prefab
|
|
112
119
|
end
|
113
120
|
end
|
114
121
|
|
115
|
-
|
116
122
|
def load_deltas(deltas, source)
|
117
123
|
deltas.deltas.each do |delta|
|
118
124
|
@config_loader.set(delta)
|
@@ -153,7 +159,7 @@ module Prefab
|
|
153
159
|
# Setup a streaming connection to the API
|
154
160
|
# Save new config values into the loader
|
155
161
|
def start_api_connection_thread(start_at_id)
|
156
|
-
config_req = Prefab::ConfigServicePointer.new(
|
162
|
+
config_req = Prefab::ConfigServicePointer.new(project_id: @base_client.project_id,
|
157
163
|
start_at_id: start_at_id)
|
158
164
|
@base_client.log_internal Logger::DEBUG, "start api connection thread #{start_at_id}"
|
159
165
|
@base_client.stats.increment("prefab.config.api.start")
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Prefab
|
2
|
+
module ConfigHelper
|
3
|
+
def value_of(config_value)
|
4
|
+
case config_value.type
|
5
|
+
when :string
|
6
|
+
config_value.string
|
7
|
+
when :int
|
8
|
+
config_value.int
|
9
|
+
when :double
|
10
|
+
config_value.double
|
11
|
+
when :bool
|
12
|
+
config_value.bool
|
13
|
+
when :feature_flag
|
14
|
+
config_value.feature_flag
|
15
|
+
when :segment
|
16
|
+
config_value.segment
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/prefab/config_loader.rb
CHANGED
@@ -14,7 +14,7 @@ module Prefab
|
|
14
14
|
def calc_config
|
15
15
|
rtn = @classpath_config.clone
|
16
16
|
@api_config.each_key do |k|
|
17
|
-
rtn[k] = @api_config[k]
|
17
|
+
rtn[k] = @api_config[k]
|
18
18
|
end
|
19
19
|
rtn = rtn.merge(@local_overrides)
|
20
20
|
rtn
|
@@ -26,7 +26,7 @@ module Prefab
|
|
26
26
|
return
|
27
27
|
end
|
28
28
|
|
29
|
-
if delta.
|
29
|
+
if delta.default.nil?
|
30
30
|
@api_config.delete(delta.key)
|
31
31
|
else
|
32
32
|
@api_config[delta.key] = delta
|
@@ -63,7 +63,7 @@ module Prefab
|
|
63
63
|
Dir.glob(glob).each do |file|
|
64
64
|
yaml = load(file)
|
65
65
|
yaml.each do |k, v|
|
66
|
-
rtn[k] = Prefab::ConfigValue.new(value_from(v))
|
66
|
+
rtn[k] = Prefab::ConfigDelta.new(key: k, default: Prefab::ConfigValue.new(value_from(v)))
|
67
67
|
end
|
68
68
|
end
|
69
69
|
rtn
|
@@ -1,11 +1,12 @@
|
|
1
1
|
module Prefab
|
2
2
|
class ConfigResolver
|
3
|
+
include Prefab::ConfigHelper
|
3
4
|
NAMESPACE_DELIMITER = ".".freeze
|
4
|
-
NAME_KEY_DELIMITER = ":".freeze
|
5
5
|
|
6
6
|
def initialize(base_client, config_loader)
|
7
7
|
@lock = Concurrent::ReadWriteLock.new
|
8
8
|
@local_store = {}
|
9
|
+
@environment = base_client.environment
|
9
10
|
@namespace = base_client.namespace
|
10
11
|
@config_loader = config_loader
|
11
12
|
make_local
|
@@ -16,7 +17,7 @@ module Prefab
|
|
16
17
|
@lock.with_read_lock do
|
17
18
|
@local_store.each do |k, v|
|
18
19
|
value = v[:value]
|
19
|
-
str << "|#{k}|
|
20
|
+
str << "|#{k}| from #{v[:match]} |#{value_of(value)}|#{value_of(value).class}\n"
|
20
21
|
end
|
21
22
|
end
|
22
23
|
str
|
@@ -39,21 +40,6 @@ module Prefab
|
|
39
40
|
|
40
41
|
private
|
41
42
|
|
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
|
55
|
-
end
|
56
|
-
|
57
43
|
# Should client a.b.c see key in namespace a.b? yes
|
58
44
|
# Should client a.b.c see key in namespace a.b.c? yes
|
59
45
|
# Should client a.b.c see key in namespace a.b.d? no
|
@@ -61,32 +47,50 @@ module Prefab
|
|
61
47
|
#
|
62
48
|
def starts_with_ns?(key_namespace, client_namespace)
|
63
49
|
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
|
50
|
+
mapped = zipped.map do |k, c|
|
51
|
+
(k.nil? || k.empty?) || k == c
|
52
|
+
end
|
53
|
+
[mapped.all?, mapped.size]
|
67
54
|
end
|
68
55
|
|
69
56
|
def make_local
|
70
57
|
store = {}
|
71
|
-
@config_loader.calc_config.each do |
|
72
|
-
|
73
|
-
|
58
|
+
@config_loader.calc_config.each do |key, delta|
|
59
|
+
# start with the top level default
|
60
|
+
to_store = { match: "default", value: delta.default }
|
61
|
+
if delta.envs.any?
|
62
|
+
env_values = delta.envs.select { |e| e.environment == @environment }
|
74
63
|
|
75
|
-
|
64
|
+
# do we have and env_values that match our env?
|
65
|
+
if env_values.any?
|
66
|
+
env_value = env_values.first
|
76
67
|
|
77
|
-
|
78
|
-
|
79
|
-
key_namespace = split[0]
|
80
|
-
end
|
68
|
+
# override the top level default with env default
|
69
|
+
to_store = { match: "env_default", env: env_value.environment, value: env_value.default }
|
81
70
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
71
|
+
if env_value.namespace_values.any?
|
72
|
+
# check all namespace_values for match
|
73
|
+
env_value.namespace_values.each do |namespace_value|
|
74
|
+
(starts_with, count) = starts_with_ns?(namespace_value.namespace, @namespace)
|
75
|
+
if starts_with
|
76
|
+
# is this match the best match?
|
77
|
+
if count > (to_store[:match_depth_count] || 0)
|
78
|
+
to_store = { match: namespace_value.namespace, count: count, value: namespace_value.config_value }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
88
83
|
end
|
89
84
|
end
|
85
|
+
|
86
|
+
# feature flags are a funny case
|
87
|
+
# we only define the variants in the default in order to be DRY
|
88
|
+
# but we want to access them in environments, clone them over
|
89
|
+
if to_store[:value].type == :feature_flag
|
90
|
+
to_store[:value].feature_flag.variants = delta.default.feature_flag.variants
|
91
|
+
end
|
92
|
+
|
93
|
+
store[key] = to_store
|
90
94
|
end
|
91
95
|
@lock.with_write_lock do
|
92
96
|
@local_store = store
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module Prefab
|
2
2
|
class FeatureFlagClient
|
3
|
+
include Prefab::ConfigHelper
|
3
4
|
MAX_32_FLOAT = 4294967294.0
|
5
|
+
DISTRIBUTION_SPACE = 1000
|
4
6
|
|
5
7
|
def initialize(base_client)
|
6
8
|
@base_client = base_client
|
@@ -14,38 +16,124 @@ module Prefab
|
|
14
16
|
feature_is_on_for?(feature_name, nil)
|
15
17
|
end
|
16
18
|
|
17
|
-
def feature_is_on_for?(feature_name, lookup_key, attributes:
|
19
|
+
def feature_is_on_for?(feature_name, lookup_key, attributes: {})
|
18
20
|
@base_client.stats.increment("prefab.featureflag.on", tags: ["feature:#{feature_name}"])
|
19
21
|
|
22
|
+
return is_on?(get(feature_name, lookup_key, attributes))
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(feature_name, lookup_key, attributes)
|
20
26
|
feature_obj = @base_client.config_client.get(feature_name)
|
21
|
-
|
27
|
+
evaluate(feature_name, lookup_key, attributes, feature_obj)
|
28
|
+
end
|
29
|
+
|
30
|
+
def evaluate(feature_name, lookup_key, attributes, feature_obj)
|
31
|
+
value_of(get_variant(feature_name, lookup_key, attributes, feature_obj))
|
22
32
|
end
|
23
33
|
|
24
34
|
private
|
25
35
|
|
26
|
-
def is_on?(
|
27
|
-
if
|
36
|
+
def is_on?(variant)
|
37
|
+
if variant.nil?
|
28
38
|
return false
|
29
39
|
end
|
40
|
+
variant.bool
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_variant(feature_name, lookup_key, attributes, feature_obj)
|
44
|
+
if !feature_obj.active
|
45
|
+
return get_variant_obj(feature_obj, feature_obj.inactive_variant_idx)
|
46
|
+
end
|
47
|
+
|
48
|
+
variant_distribution = feature_obj.default
|
49
|
+
|
50
|
+
# if user_targets.match
|
51
|
+
feature_obj.user_targets.each do |target|
|
52
|
+
if (target.identifiers.include? lookup_key)
|
53
|
+
return get_variant_obj(feature_obj, target.variant_idx)
|
54
|
+
end
|
55
|
+
end
|
30
56
|
|
31
|
-
|
32
|
-
|
33
|
-
|
57
|
+
# if rules.match
|
58
|
+
feature_obj.rules.each do |rule|
|
59
|
+
if criteria_match?(rule, lookup_key, attributes)
|
60
|
+
variant_distribution = rule.distribution
|
61
|
+
end
|
34
62
|
end
|
35
63
|
|
36
|
-
if
|
37
|
-
|
64
|
+
if variant_distribution.type == :variant_idx
|
65
|
+
variant_idx = variant_distribution.variant_idx
|
66
|
+
else
|
67
|
+
percent_through_distribution = rand()
|
68
|
+
if lookup_key
|
69
|
+
percent_through_distribution = get_user_pct(feature_name, lookup_key)
|
70
|
+
end
|
71
|
+
distribution_bucket = DISTRIBUTION_SPACE * percent_through_distribution
|
72
|
+
|
73
|
+
variant_idx = get_variant_idx_from_weights(variant_distribution.variant_weights.weights, distribution_bucket, feature_name)
|
38
74
|
end
|
39
75
|
|
40
|
-
return feature_obj
|
76
|
+
return get_variant_obj(feature_obj, variant_idx)
|
77
|
+
end
|
78
|
+
|
79
|
+
def get_variant_obj(feature_obj, idx)
|
80
|
+
return feature_obj.variants[idx] if feature_obj.variants.length >= idx
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_variant_idx_from_weights(variant_weights, bucket, feature_name)
|
85
|
+
sum = 0
|
86
|
+
variant_weights.each do |variant_weight|
|
87
|
+
if bucket < sum + variant_weight.weight
|
88
|
+
return variant_weight.variant_idx
|
89
|
+
else
|
90
|
+
sum += variant_weight.weight
|
91
|
+
end
|
92
|
+
end
|
93
|
+
# variants didn't add up to 100%
|
94
|
+
@base_client.log.info("Variants of #{feature_name} did not add to 100%")
|
95
|
+
return variant_weights.last.variant
|
41
96
|
end
|
42
97
|
|
43
98
|
def get_user_pct(feature, lookup_key)
|
44
|
-
to_hash = "#{@base_client.
|
99
|
+
to_hash = "#{@base_client.project_id}#{feature}#{lookup_key}"
|
45
100
|
int_value = Murmur3.murmur3_32(to_hash)
|
46
101
|
int_value / MAX_32_FLOAT
|
47
102
|
end
|
48
103
|
|
104
|
+
def criteria_match?(rule, lookup_key, attributes)
|
105
|
+
if rule.criteria.operator == :IN
|
106
|
+
return rule.criteria.values.include?(lookup_key)
|
107
|
+
elsif rule.criteria.operator == :NOT_IN
|
108
|
+
return !rule.criteria.values.include?(lookup_key)
|
109
|
+
elsif rule.criteria.operator == :IN_SEG
|
110
|
+
return segment_matches(rule.criteria.values, lookup_key, attributes).any?
|
111
|
+
elsif rule.criteria.operator == :NOT_IN_SEG
|
112
|
+
return segment_matches(rule.criteria.values, lookup_key, attributes).none?
|
113
|
+
end
|
114
|
+
@base_client.log.info("Unknown Operator")
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
118
|
+
# evaluate each segment key and return whether each one matches
|
119
|
+
# there should be an associated segment available as a standard config obj
|
120
|
+
def segment_matches(segment_keys, lookup_key, attributes)
|
121
|
+
segment_keys.map do |segment_key|
|
122
|
+
segment = @base_client.config_client.get(segment_key)
|
123
|
+
if segment.nil?
|
124
|
+
@base_client.log.info("Missing Segment")
|
125
|
+
false
|
126
|
+
else
|
127
|
+
segment_match?(segment, lookup_key, attributes)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def segment_match?(segment, lookup_key, attributes)
|
133
|
+
includes = segment.includes.include?(lookup_key)
|
134
|
+
excludes = segment.excludes.include?(lookup_key)
|
135
|
+
includes && !excludes
|
136
|
+
end
|
49
137
|
end
|
50
138
|
end
|
51
139
|
|