prefab-cloud-ruby 0.0.9 → 0.0.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -2
- data/Gemfile.lock +5 -5
- data/VERSION +1 -1
- data/lib/prefab/client.rb +14 -6
- data/lib/prefab/config_client.rb +120 -20
- data/lib/prefab/config_loader.rb +44 -18
- data/lib/prefab/config_resolver.rb +11 -12
- data/lib/prefab/feature_flag_client.rb +8 -8
- data/lib/prefab/ratelimit_client.rb +11 -6
- data/lib/prefab/retry.rb +5 -3
- data/lib/prefab_pb.rb +0 -3
- data/prefab-cloud-ruby.gemspec +15 -11
- data/test/.prefab.test.config.yaml +5 -0
- data/test/test_config_loader.rb +43 -0
- data/test/test_config_resolver.rb +44 -0
- data/test/test_feature_flag_client.rb +64 -0
- data/test/test_helper.rb +21 -0
- metadata +11 -7
- data/lib/prefab/store.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e4ae024730e476fe81b2d5e1130bb13e0dfee4f6
|
4
|
+
data.tar.gz: aa819d0f9c64ffcd8826751d176133fce7a1a57e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c10adec4b6866caee1045830c4ad45f2f36b20011e3ed8c3d4670331e62f0b65b3b57bd91c0c19dbe9ce4e164787cc5717d622fbe8cbe6cbbca21e03e54bfcf0
|
7
|
+
data.tar.gz: e75c1d4c816f403e974e7b1f105d233fa1578e667d4cfaa804daea81e9ba633478cdba059dab2705ddc45721b3ee734edf1dd2d52dbb1990cc16bd4f28ef203b
|
data/Gemfile
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
source "https://rubygems.org"
|
2
2
|
|
3
|
-
gem 'grpc', '~> 1.
|
3
|
+
gem 'grpc', '~> 1.8.3'
|
4
4
|
gem 'concurrent-ruby', '~> 1.0', '>= 1.0.5'
|
5
5
|
|
6
6
|
group :development do
|
7
|
-
gem 'grpc-tools', '~> 1.8'
|
7
|
+
gem 'grpc-tools', '~> 1.8.3'
|
8
8
|
gem "shoulda", ">= 0"
|
9
9
|
gem "rdoc", "~> 3.12"
|
10
10
|
gem "bundler", "~> 1.0"
|
data/Gemfile.lock
CHANGED
@@ -22,7 +22,7 @@ GEM
|
|
22
22
|
faraday (~> 0.8)
|
23
23
|
hashie (~> 3.5, >= 3.5.2)
|
24
24
|
oauth2 (~> 1.0)
|
25
|
-
google-protobuf (3.5.
|
25
|
+
google-protobuf (3.5.1)
|
26
26
|
googleapis-common-protos-types (1.0.1)
|
27
27
|
google-protobuf (~> 3.0)
|
28
28
|
googleauth (0.6.2)
|
@@ -33,11 +33,11 @@ GEM
|
|
33
33
|
multi_json (~> 1.11)
|
34
34
|
os (~> 0.9)
|
35
35
|
signet (~> 0.7)
|
36
|
-
grpc (1.
|
36
|
+
grpc (1.8.3)
|
37
37
|
google-protobuf (~> 3.1)
|
38
38
|
googleapis-common-protos-types (~> 1.0.0)
|
39
39
|
googleauth (>= 0.5.1, < 0.7)
|
40
|
-
grpc-tools (1.8.
|
40
|
+
grpc-tools (1.8.3)
|
41
41
|
hashie (3.5.6)
|
42
42
|
highline (1.7.10)
|
43
43
|
i18n (0.9.1)
|
@@ -105,8 +105,8 @@ PLATFORMS
|
|
105
105
|
DEPENDENCIES
|
106
106
|
bundler (~> 1.0)
|
107
107
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
108
|
-
grpc (~> 1.
|
109
|
-
grpc-tools (~> 1.8)
|
108
|
+
grpc (~> 1.8.3)
|
109
|
+
grpc-tools (~> 1.8.3)
|
110
110
|
juwelier (~> 2.1.0)
|
111
111
|
rdoc (~> 3.12)
|
112
112
|
shoulda
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.10
|
data/lib/prefab/client.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Prefab
|
2
2
|
class Client
|
3
|
-
attr_reader :account_id, :shared_cache, :stats, :namespace, :logger, :creds, :
|
3
|
+
attr_reader :account_id, :shared_cache, :stats, :namespace, :logger, :creds, :interceptor
|
4
4
|
|
5
|
-
def initialize(api_key
|
5
|
+
def initialize(api_key: ENV['PREFAB_API_KEY'],
|
6
6
|
logger: nil,
|
7
7
|
stats: nil, # receives increment("prefab.limitcheck", {:tags=>["policy_group:page_view", "pass:true"]})
|
8
8
|
shared_cache: nil, # Something that quacks like Rails.cache ideally memcached
|
@@ -12,6 +12,7 @@ module Prefab
|
|
12
12
|
@logger = (logger || Logger.new($stdout)).tap do |log|
|
13
13
|
log.progname = "Prefab" if log.respond_to? :progname=
|
14
14
|
end
|
15
|
+
@local = local
|
15
16
|
@stats = (stats || NoopStats.new)
|
16
17
|
@shared_cache = (shared_cache || NoopCache.new)
|
17
18
|
@account_id = api_key.split("|")[0].to_i
|
@@ -20,10 +21,17 @@ module Prefab
|
|
20
21
|
@interceptor = AuthInterceptor.new(api_key)
|
21
22
|
|
22
23
|
@creds = GRPC::Core::ChannelCredentials.new(ssl_certs)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
end
|
25
|
+
|
26
|
+
def channel
|
27
|
+
@_channel ||= @local ?
|
28
|
+
GRPC::Core::Channel.new('localhost:8443', nil, :this_channel_is_insecure)
|
29
|
+
:
|
30
|
+
GRPC::Core::Channel.new('api.prefab.cloud:8443', nil, @creds)
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset_channel!
|
34
|
+
@_channel = nil
|
27
35
|
end
|
28
36
|
|
29
37
|
def config_client(timeout: 5.0)
|
data/lib/prefab/config_client.rb
CHANGED
@@ -1,61 +1,161 @@
|
|
1
1
|
module Prefab
|
2
2
|
class ConfigClient
|
3
3
|
RECONNECT_WAIT = 5
|
4
|
+
CHECKPOINT_FREQ_SEC = 10
|
5
|
+
SUSPENDERS_FREQ_SEC = 60
|
4
6
|
|
5
7
|
def initialize(base_client, timeout)
|
6
8
|
@base_client = base_client
|
7
9
|
@timeout = timeout
|
8
|
-
@
|
9
|
-
|
10
|
+
@config_loader = Prefab::ConfigLoader.new(base_client)
|
11
|
+
@config_resolver = Prefab::ConfigResolver.new(base_client, @config_loader)
|
12
|
+
start_at_id = load_checkpoint
|
13
|
+
start_api_connection_thread(start_at_id)
|
14
|
+
start_checkpointing_thread
|
15
|
+
start_suspenders_thread
|
10
16
|
end
|
11
17
|
|
12
18
|
def get(prop)
|
13
19
|
@config_resolver.get(prop)
|
14
20
|
end
|
15
21
|
|
16
|
-
def set(
|
17
|
-
|
18
|
-
|
22
|
+
def set(key, config_value, namespace = nil)
|
23
|
+
raise "key must not contain ':' set namespaces separately" if key.include? ":"
|
24
|
+
raise "namespace must not contain ':'" if namespace&.include?(":")
|
25
|
+
config_delta = Prefab::ConfigClient.value_to_delta(key, config_value, namespace)
|
26
|
+
Retry.it method(:stub_with_timeout), :upsert, config_delta, @timeout, method(:reset)
|
27
|
+
@config_loader.set(config_delta)
|
28
|
+
end
|
29
|
+
|
30
|
+
def reset
|
31
|
+
@base_client.reset_channel!
|
32
|
+
@_stub = nil
|
33
|
+
@_stub_with_timeout = nil
|
19
34
|
end
|
20
35
|
|
21
36
|
def to_s
|
22
37
|
@config_resolver.to_s
|
23
38
|
end
|
24
39
|
|
40
|
+
def self.value_to_delta(key, config_value, namespace = nil)
|
41
|
+
Prefab::ConfigDelta.new(key: [namespace, key].compact.join(":"),
|
42
|
+
value: config_value)
|
43
|
+
end
|
44
|
+
|
25
45
|
private
|
26
46
|
|
27
47
|
def stub
|
28
|
-
Prefab::ConfigService::Stub.new(nil,
|
29
|
-
|
30
|
-
|
31
|
-
|
48
|
+
@_stub = Prefab::ConfigService::Stub.new(nil,
|
49
|
+
nil,
|
50
|
+
channel_override: @base_client.channel,
|
51
|
+
interceptors: [@base_client.interceptor])
|
32
52
|
end
|
33
53
|
|
34
|
-
def
|
35
|
-
Prefab::ConfigService::Stub.new(nil,
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
54
|
+
def stub_with_timeout
|
55
|
+
@_stub_with_timeout = Prefab::ConfigService::Stub.new(nil,
|
56
|
+
nil,
|
57
|
+
channel_override: @base_client.channel,
|
58
|
+
timeout: @timeout,
|
59
|
+
interceptors: [@base_client.interceptor])
|
40
60
|
end
|
41
61
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
62
|
+
def cache_key
|
63
|
+
"prefab:config:checkpoint"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Bootstrap out of the cache
|
67
|
+
# returns the high-watermark of what was in the cache
|
68
|
+
def load_checkpoint
|
69
|
+
checkpoint = @base_client.shared_cache.read(cache_key)
|
70
|
+
start_at_id = 0
|
71
|
+
|
72
|
+
if checkpoint
|
73
|
+
deltas = Prefab::ConfigDeltas.decode(checkpoint)
|
74
|
+
deltas.deltas.each do |delta|
|
75
|
+
@base_client.logger.debug "checkpoint set #{delta.key} #{delta.value.int} #{delta.value.string} #{delta.id} "
|
76
|
+
@config_loader.set(delta)
|
77
|
+
start_at_id = [delta.id, start_at_id].max
|
78
|
+
end
|
79
|
+
@base_client.logger.info "Found checkpoint with highwater id #{start_at_id}"
|
80
|
+
@config_resolver.update
|
81
|
+
else
|
82
|
+
@base_client.logger.info "No checkpoint"
|
83
|
+
end
|
84
|
+
|
85
|
+
start_at_id
|
86
|
+
end
|
87
|
+
|
88
|
+
# A thread that saves current state to the cache, "checkpointing" it
|
89
|
+
def start_checkpointing_thread
|
90
|
+
Thread.new do
|
91
|
+
loop do
|
92
|
+
begin
|
93
|
+
started_at = Time.now
|
94
|
+
deltas = @config_resolver.export_api_deltas
|
95
|
+
@base_client.logger.debug "==CHECKPOINT==#{deltas.deltas.map {|d| d.id}.max}====="
|
96
|
+
@base_client.shared_cache.write(cache_key, Prefab::ConfigDeltas.encode(deltas))
|
97
|
+
delta = CHECKPOINT_FREQ_SEC - (Time.now - started_at)
|
98
|
+
if delta > 0
|
99
|
+
sleep(delta)
|
100
|
+
end
|
101
|
+
rescue StandardError => exn
|
102
|
+
@base_client.logger.info "Issue Checkpointing #{exn.message}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
45
107
|
|
108
|
+
# Setup a streaming connection to the API
|
109
|
+
# Save new config values into the loader
|
110
|
+
def start_api_connection_thread(start_at_id)
|
111
|
+
config_req = Prefab::ConfigServicePointer.new(account_id: @base_client.account_id,
|
112
|
+
start_at_id: start_at_id)
|
46
113
|
Thread.new do
|
47
114
|
while true do
|
48
115
|
begin
|
49
116
|
resp = stub.get_config(config_req)
|
50
117
|
resp.each do |r|
|
51
118
|
r.deltas.each do |delta|
|
52
|
-
@
|
119
|
+
@config_loader.set(delta)
|
53
120
|
end
|
54
121
|
@config_resolver.update
|
55
122
|
end
|
56
123
|
rescue => e
|
57
|
-
sleep(RECONNECT_WAIT)
|
58
124
|
@base_client.logger.info("config client encountered #{e.message} pausing #{RECONNECT_WAIT}")
|
125
|
+
reset
|
126
|
+
sleep(RECONNECT_WAIT)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Streaming connections don't guarantee all items have been seen
|
133
|
+
#
|
134
|
+
def start_suspenders_thread
|
135
|
+
start_at_suspenders = 0
|
136
|
+
|
137
|
+
Thread.new do
|
138
|
+
loop do
|
139
|
+
begin
|
140
|
+
started_at = Time.now
|
141
|
+
config_req = Prefab::ConfigServicePointer.new(account_id: @base_client.account_id,
|
142
|
+
start_at_id: start_at_suspenders)
|
143
|
+
resp = stub_with_timeout.get_config(config_req)
|
144
|
+
resp.each do |r|
|
145
|
+
r.deltas.each do |delta|
|
146
|
+
@config_loader.set(delta)
|
147
|
+
start_at_suspenders = [start_at_suspenders, delta.id].max
|
148
|
+
end
|
149
|
+
end
|
150
|
+
rescue GRPC::DeadlineExceeded
|
151
|
+
# Ignore. This is a streaming endpoint, but we only need a single response
|
152
|
+
rescue => e
|
153
|
+
@base_client.logger.info "Suspenders encountered an issue #{e.message}"
|
154
|
+
end
|
155
|
+
|
156
|
+
delta = SUSPENDERS_FREQ_SEC - (Time.now - started_at)
|
157
|
+
if delta > 0
|
158
|
+
sleep(delta)
|
59
159
|
end
|
60
160
|
end
|
61
161
|
end
|
data/lib/prefab/config_loader.rb
CHANGED
@@ -1,51 +1,77 @@
|
|
1
1
|
require 'yaml'
|
2
|
-
module
|
2
|
+
module Prefab
|
3
3
|
class ConfigLoader
|
4
|
-
def initialize(
|
5
|
-
@
|
6
|
-
|
7
|
-
|
8
|
-
load_local_overrides
|
4
|
+
def initialize(base_client)
|
5
|
+
@base_client = base_client
|
6
|
+
@classpath_config = load_classpath_config
|
7
|
+
@local_overrides = load_local_overrides
|
9
8
|
@api_config = Concurrent::Map.new
|
10
|
-
@immutable_config = @ez_config.merge(@project_config)
|
11
9
|
end
|
12
10
|
|
13
11
|
def calc_config
|
14
|
-
rtn = @
|
12
|
+
rtn = @classpath_config.clone
|
15
13
|
@api_config.each_key do |k|
|
16
|
-
rtn[k] = @api_config[k]
|
14
|
+
rtn[k] = @api_config[k].value
|
17
15
|
end
|
18
16
|
rtn = rtn.merge(@local_overrides)
|
19
17
|
rtn
|
20
18
|
end
|
21
19
|
|
22
20
|
def set(delta)
|
23
|
-
@api_config[delta.key] = delta
|
21
|
+
@api_config[delta.key] = delta
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_api_deltas
|
25
|
+
deltas = Prefab::ConfigDeltas.new
|
26
|
+
@api_config.each_value do |config_value|
|
27
|
+
deltas.deltas << config_value
|
28
|
+
end
|
29
|
+
deltas
|
24
30
|
end
|
25
31
|
|
26
32
|
private
|
27
33
|
|
28
|
-
def
|
29
|
-
|
34
|
+
def load_classpath_config
|
35
|
+
classpath_dir = ENV['PREFAB_CONFIG_CLASSPATH_DIR'] || ""
|
36
|
+
load_glob(File.join(classpath_dir, ".prefab*config.yaml"))
|
30
37
|
end
|
31
38
|
|
32
|
-
def
|
33
|
-
|
39
|
+
def load_local_overrides
|
40
|
+
override_dir = ENV['PREFAB_CONFIG_OVERRIDE_DIR'] || Dir.home
|
41
|
+
load_glob(File.join(override_dir, ".prefab*config.yaml"))
|
34
42
|
end
|
35
43
|
|
36
|
-
def
|
37
|
-
|
44
|
+
def load_glob(glob)
|
45
|
+
rtn = {}
|
46
|
+
Dir.glob(glob).each do |file|
|
47
|
+
yaml = load(file)
|
48
|
+
yaml.each do |k, v|
|
49
|
+
rtn[k] = Prefab::ConfigValue.new(value_from(v))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
rtn
|
38
53
|
end
|
39
54
|
|
40
55
|
def load(filename)
|
41
56
|
if File.exist? filename
|
42
|
-
|
43
57
|
YAML.load_file(filename)
|
44
58
|
else
|
45
|
-
@logger.info "No file #{filename}"
|
59
|
+
@base_client.logger.info "No file #{filename}"
|
46
60
|
{}
|
47
61
|
end
|
48
62
|
end
|
49
63
|
|
64
|
+
def value_from(raw)
|
65
|
+
case raw
|
66
|
+
when String
|
67
|
+
{string: raw}
|
68
|
+
when Integer
|
69
|
+
{int: raw}
|
70
|
+
when TrueClass, FalseClass
|
71
|
+
{bool: raw}
|
72
|
+
when Float
|
73
|
+
{double: raw}
|
74
|
+
end
|
75
|
+
end
|
50
76
|
end
|
51
77
|
end
|
@@ -1,12 +1,12 @@
|
|
1
|
-
module
|
1
|
+
module Prefab
|
2
2
|
class ConfigResolver
|
3
3
|
|
4
|
-
def initialize(
|
4
|
+
def initialize(base_client, config_loader)
|
5
5
|
@lock = Concurrent::ReadWriteLock.new
|
6
6
|
@local_store = {}
|
7
|
-
@namespace =
|
8
|
-
@config_loader =
|
9
|
-
@logger =
|
7
|
+
@namespace = base_client.namespace
|
8
|
+
@config_loader = config_loader
|
9
|
+
@logger = base_client.logger
|
10
10
|
make_local
|
11
11
|
end
|
12
12
|
|
@@ -15,7 +15,7 @@ module EzConfig
|
|
15
15
|
@lock.with_read_lock do
|
16
16
|
@local_store.each do |k, v|
|
17
17
|
value = v[:value]
|
18
|
-
str << "|#{k}| |#{value_of(value)}
|
18
|
+
str << "|#{k}| |#{value_of(value)}|#{value_of(value).class}\n"
|
19
19
|
end
|
20
20
|
end
|
21
21
|
str
|
@@ -28,15 +28,14 @@ module EzConfig
|
|
28
28
|
config ? value_of(config[:value]) : nil
|
29
29
|
end
|
30
30
|
|
31
|
-
def set(delta, do_update: true)
|
32
|
-
@config_loader.set(delta)
|
33
|
-
update if do_update
|
34
|
-
end
|
35
|
-
|
36
31
|
def update
|
37
32
|
make_local
|
38
33
|
end
|
39
34
|
|
35
|
+
def export_api_deltas
|
36
|
+
@config_loader.get_api_deltas
|
37
|
+
end
|
38
|
+
|
40
39
|
private
|
41
40
|
|
42
41
|
def value_of(config_value)
|
@@ -66,7 +65,7 @@ module EzConfig
|
|
66
65
|
namespace = split[0]
|
67
66
|
end
|
68
67
|
|
69
|
-
if (namespace == "") || namespace.start_with?(
|
68
|
+
if (namespace == "") || @namespace.start_with?(namespace)
|
70
69
|
existing = store[property]
|
71
70
|
if existing.nil?
|
72
71
|
store[property] = { namespace: namespace, value: value }
|
@@ -7,10 +7,7 @@ module Prefab
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def upsert(feature_obj)
|
10
|
-
|
11
|
-
key: feature_config_name(feature_obj.feature),
|
12
|
-
value: Prefab::ConfigValue.new(feature_flag: feature_obj))
|
13
|
-
@base_client.config_client.set(delta)
|
10
|
+
@base_client.config_client.set(feature_config_name(feature_obj.feature), Prefab::ConfigValue.new(feature_flag: feature_obj))
|
14
11
|
end
|
15
12
|
|
16
13
|
def feature_is_on?(feature_name)
|
@@ -21,6 +18,12 @@ module Prefab
|
|
21
18
|
@base_client.stats.increment("prefab.featureflag.on", tags: ["feature:#{feature_name}"])
|
22
19
|
|
23
20
|
feature_obj = @base_client.config_client.get(feature_config_name(feature_name))
|
21
|
+
return is_on?(feature_name, lookup_key, attributes, feature_obj)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def is_on?(feature_name, lookup_key, attributes, feature_obj)
|
24
27
|
if feature_obj.nil?
|
25
28
|
return false
|
26
29
|
end
|
@@ -37,11 +40,8 @@ module Prefab
|
|
37
40
|
return feature_obj.pct > rand()
|
38
41
|
end
|
39
42
|
|
40
|
-
private
|
41
|
-
|
42
|
-
|
43
43
|
def get_user_pct(feature, lookup_key)
|
44
|
-
int_value = Murmur3.murmur3_32("#{@account_id}#{feature}#{lookup_key}")
|
44
|
+
int_value = Murmur3.murmur3_32("#{@base_client.account_id}#{feature}#{lookup_key}")
|
45
45
|
int_value / MAX_32_FLOAT
|
46
46
|
end
|
47
47
|
|
@@ -26,7 +26,7 @@ module Prefab
|
|
26
26
|
allow_partial_response: allow_partial_response
|
27
27
|
)
|
28
28
|
|
29
|
-
result = Retry.it(method(:stub), :limit_check, req, @timeout)
|
29
|
+
result = Retry.it(method(:stub), :limit_check, req, @timeout, method(:reset))
|
30
30
|
|
31
31
|
reset = result.limit_reset_at
|
32
32
|
@base_client.shared_cache.write(expiry_cache_key, reset) unless reset < 1 # protobuf default int to 0
|
@@ -41,12 +41,17 @@ module Prefab
|
|
41
41
|
|
42
42
|
private
|
43
43
|
|
44
|
+
def reset
|
45
|
+
@base_client.reset_channel!
|
46
|
+
@_stub = nil
|
47
|
+
end
|
48
|
+
|
44
49
|
def stub
|
45
|
-
Prefab::RateLimitService::Stub.new(nil,
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
+
@_stub ||= Prefab::RateLimitService::Stub.new(nil,
|
51
|
+
nil,
|
52
|
+
channel_override: @base_client.channel,
|
53
|
+
timeout: @timeout,
|
54
|
+
interceptors: [@base_client.interceptor])
|
50
55
|
end
|
51
56
|
|
52
57
|
def handle_error(e, on_error, groups)
|
data/lib/prefab/retry.rb
CHANGED
@@ -5,21 +5,23 @@ class Retry
|
|
5
5
|
|
6
6
|
# GRPC Generally handles timeouts for us
|
7
7
|
# but if the connection is broken we want to retry up until the timeout
|
8
|
-
def self.it(stub_factory, rpc, req, timeout)
|
8
|
+
def self.it(stub_factory, rpc, req, timeout, reset)
|
9
9
|
attempts = 0
|
10
10
|
start_time = Time.now
|
11
11
|
|
12
12
|
begin
|
13
13
|
attempts += 1
|
14
|
-
|
15
14
|
return stub_factory.call.send(rpc, req)
|
16
15
|
rescue => exception
|
17
|
-
|
16
|
+
if Time.now - start_time > timeout
|
17
|
+
raise exception
|
18
|
+
end
|
18
19
|
|
19
20
|
sleep_seconds = [BASE_SLEEP_SEC * (2 ** (attempts - 1)), MAX_SLEEP_SEC].min
|
20
21
|
sleep_seconds = sleep_seconds * (0.5 * (1 + rand()))
|
21
22
|
sleep_seconds = [BASE_SLEEP_SEC, sleep_seconds].max
|
22
23
|
sleep sleep_seconds
|
24
|
+
reset.call
|
23
25
|
retry
|
24
26
|
end
|
25
27
|
end
|
data/lib/prefab_pb.rb
CHANGED
@@ -9,7 +9,6 @@ Google::Protobuf::DescriptorPool.generated_pool.build do
|
|
9
9
|
optional :start_at_id, :int64, 2
|
10
10
|
end
|
11
11
|
add_message "prefab.ConfigDelta" do
|
12
|
-
optional :account_id, :int64, 1
|
13
12
|
optional :id, :int64, 2
|
14
13
|
optional :key, :string, 3
|
15
14
|
optional :value, :message, 4, "prefab.ConfigValue"
|
@@ -62,11 +61,9 @@ Google::Protobuf::DescriptorPool.generated_pool.build do
|
|
62
61
|
value :MAXIMUM, 2
|
63
62
|
end
|
64
63
|
add_message "prefab.FeatureFlag" do
|
65
|
-
optional :account_id, :int64, 1
|
66
64
|
optional :feature, :string, 2
|
67
65
|
optional :pct, :double, 3
|
68
66
|
repeated :whitelisted, :string, 4
|
69
|
-
optional :prior_feature_name, :string, 5
|
70
67
|
end
|
71
68
|
add_message "prefab.LimitDefinition" do
|
72
69
|
optional :group, :string, 1
|
data/prefab-cloud-ruby.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: prefab-cloud-ruby 0.0.
|
5
|
+
# stub: prefab-cloud-ruby 0.0.10 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "prefab-cloud-ruby".freeze
|
9
|
-
s.version = "0.0.
|
9
|
+
s.version = "0.0.10"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
13
13
|
s.authors = ["Jeff Dwyer".freeze]
|
14
|
-
s.date = "
|
14
|
+
s.date = "2018-01-12"
|
15
15
|
s.description = "RateLimits & Config as a service".freeze
|
16
16
|
s.email = "jdwyer@prefab.cloud".freeze
|
17
17
|
s.extra_rdoc_files = [
|
@@ -37,10 +37,14 @@ Gem::Specification.new do |s|
|
|
37
37
|
"lib/prefab/noop_stats.rb",
|
38
38
|
"lib/prefab/ratelimit_client.rb",
|
39
39
|
"lib/prefab/retry.rb",
|
40
|
-
"lib/prefab/store.rb",
|
41
40
|
"lib/prefab_pb.rb",
|
42
41
|
"lib/prefab_services_pb.rb",
|
43
|
-
"prefab-cloud-ruby.gemspec"
|
42
|
+
"prefab-cloud-ruby.gemspec",
|
43
|
+
"test/.prefab.test.config.yaml",
|
44
|
+
"test/test_config_loader.rb",
|
45
|
+
"test/test_config_resolver.rb",
|
46
|
+
"test/test_feature_flag_client.rb",
|
47
|
+
"test/test_helper.rb"
|
44
48
|
]
|
45
49
|
s.homepage = "http://github.com/prefab-cloud/prefab-cloud-ruby".freeze
|
46
50
|
s.licenses = ["MIT".freeze]
|
@@ -51,18 +55,18 @@ Gem::Specification.new do |s|
|
|
51
55
|
s.specification_version = 4
|
52
56
|
|
53
57
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
54
|
-
s.add_runtime_dependency(%q<grpc>.freeze, ["~> 1.
|
58
|
+
s.add_runtime_dependency(%q<grpc>.freeze, ["~> 1.8.3"])
|
55
59
|
s.add_runtime_dependency(%q<concurrent-ruby>.freeze, [">= 1.0.5", "~> 1.0"])
|
56
|
-
s.add_development_dependency(%q<grpc-tools>.freeze, ["~> 1.8"])
|
60
|
+
s.add_development_dependency(%q<grpc-tools>.freeze, ["~> 1.8.3"])
|
57
61
|
s.add_development_dependency(%q<shoulda>.freeze, [">= 0"])
|
58
62
|
s.add_development_dependency(%q<rdoc>.freeze, ["~> 3.12"])
|
59
63
|
s.add_development_dependency(%q<bundler>.freeze, ["~> 1.0"])
|
60
64
|
s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
|
61
65
|
s.add_development_dependency(%q<simplecov>.freeze, [">= 0"])
|
62
66
|
else
|
63
|
-
s.add_dependency(%q<grpc>.freeze, ["~> 1.
|
67
|
+
s.add_dependency(%q<grpc>.freeze, ["~> 1.8.3"])
|
64
68
|
s.add_dependency(%q<concurrent-ruby>.freeze, [">= 1.0.5", "~> 1.0"])
|
65
|
-
s.add_dependency(%q<grpc-tools>.freeze, ["~> 1.8"])
|
69
|
+
s.add_dependency(%q<grpc-tools>.freeze, ["~> 1.8.3"])
|
66
70
|
s.add_dependency(%q<shoulda>.freeze, [">= 0"])
|
67
71
|
s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
|
68
72
|
s.add_dependency(%q<bundler>.freeze, ["~> 1.0"])
|
@@ -70,9 +74,9 @@ Gem::Specification.new do |s|
|
|
70
74
|
s.add_dependency(%q<simplecov>.freeze, [">= 0"])
|
71
75
|
end
|
72
76
|
else
|
73
|
-
s.add_dependency(%q<grpc>.freeze, ["~> 1.
|
77
|
+
s.add_dependency(%q<grpc>.freeze, ["~> 1.8.3"])
|
74
78
|
s.add_dependency(%q<concurrent-ruby>.freeze, [">= 1.0.5", "~> 1.0"])
|
75
|
-
s.add_dependency(%q<grpc-tools>.freeze, ["~> 1.8"])
|
79
|
+
s.add_dependency(%q<grpc-tools>.freeze, ["~> 1.8.3"])
|
76
80
|
s.add_dependency(%q<shoulda>.freeze, [">= 0"])
|
77
81
|
s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
|
78
82
|
s.add_dependency(%q<bundler>.freeze, ["~> 1.0"])
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TestConfigLoader < Minitest::Test
|
4
|
+
def setup
|
5
|
+
ENV['PREFAB_CONFIG_OVERRIDE_DIR'] = "none"
|
6
|
+
ENV['PREFAB_CONFIG_CLASSPATH_DIR'] = "test"
|
7
|
+
@loader = Prefab::ConfigLoader.new(MockBaseClient.new)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_load
|
11
|
+
should_be :int, 123, "sample_int"
|
12
|
+
should_be :string, "OneTwoThree", "sample"
|
13
|
+
should_be :bool, true, "sample_bool"
|
14
|
+
should_be :double, 12.12, "sample_double"
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def test_api_precedence
|
19
|
+
should_be :int, 123, "sample_int"
|
20
|
+
|
21
|
+
@loader.set(Prefab::ConfigDelta.new(key: "sample_int", value: Prefab::ConfigValue.new(int: 456)))
|
22
|
+
should_be :int, 456, "sample_int"
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_api_deltas
|
27
|
+
val = Prefab::ConfigValue.new(int: 456)
|
28
|
+
delta = Prefab::ConfigDelta.new(key: "sample_int", value: val)
|
29
|
+
@loader.set(delta)
|
30
|
+
|
31
|
+
deltas = Prefab::ConfigDeltas.new
|
32
|
+
deltas.deltas << delta
|
33
|
+
assert_equal deltas, @loader.get_api_deltas
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def should_be(type, value, key)
|
39
|
+
assert_equal type, @loader.calc_config[key].type
|
40
|
+
assert_equal value, @loader.calc_config[key].send(type)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TestConfigResolver < Minitest::Test
|
4
|
+
|
5
|
+
def test_resolution
|
6
|
+
@loader = MockConfigLoader.new
|
7
|
+
|
8
|
+
loaded_values = {
|
9
|
+
"projectA:key" => Prefab::ConfigValue.new(string: "valueA"),
|
10
|
+
"key" => Prefab::ConfigValue.new(string: "value_none"),
|
11
|
+
"projectB:key" => Prefab::ConfigValue.new(string: "valueB"),
|
12
|
+
"projectB.subprojectX:key" => Prefab::ConfigValue.new(string: "projectB.subprojectX"),
|
13
|
+
"projectB.subprojectY:key" => Prefab::ConfigValue.new(string: "projectB.subprojectY"),
|
14
|
+
"projectB:key2" => Prefab::ConfigValue.new(string: "valueB2")
|
15
|
+
}
|
16
|
+
|
17
|
+
@loader.stub :calc_config, loaded_values do
|
18
|
+
@resolver = Prefab::ConfigResolver.new(MockBaseClient.new, @loader)
|
19
|
+
assert_equal "value_none", @resolver.get("key")
|
20
|
+
|
21
|
+
|
22
|
+
@resolverA = resolver_for_namespace("projectA", @loader)
|
23
|
+
assert_equal "valueA", @resolverA.get("key")
|
24
|
+
|
25
|
+
@resolverB = resolver_for_namespace("projectB", @loader)
|
26
|
+
assert_equal "valueB", @resolverB.get("key")
|
27
|
+
|
28
|
+
@resolverBX = resolver_for_namespace("projectB.subprojectX", @loader)
|
29
|
+
assert_equal "projectB.subprojectX", @resolverBX.get("key")
|
30
|
+
|
31
|
+
@resolverUndefinedSubProject = resolver_for_namespace("projectB.subprojectX:subsubQ", @loader)
|
32
|
+
assert_equal "projectB.subprojectX", @resolverBX.get("key")
|
33
|
+
|
34
|
+
@resolverBX = resolver_for_namespace("projectC", @loader)
|
35
|
+
assert_equal "value_none", @resolverBX.get("key")
|
36
|
+
assert_nil @resolverBX.get("key_that_doesnt_exist")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def resolver_for_namespace(namespace, loader)
|
41
|
+
Prefab::ConfigResolver.new(MockBaseClient.new(namespace: namespace), loader)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TestFeatureFlagClient < Minitest::Test
|
4
|
+
|
5
|
+
def test_pct
|
6
|
+
client = Prefab::FeatureFlagClient.new(MockBaseClient.new)
|
7
|
+
Prefab::FeatureFlagClient.send(:public, :is_on?)
|
8
|
+
|
9
|
+
flag = Prefab::FeatureFlag.new(feature: "FlagName", pct: 0.5)
|
10
|
+
|
11
|
+
assert_equal false,
|
12
|
+
client.is_on?(flag.feature, "hashes high", [], flag)
|
13
|
+
|
14
|
+
assert_equal true,
|
15
|
+
client.is_on?(flag.feature, "hashes low", [], flag)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def test_off
|
20
|
+
client = Prefab::FeatureFlagClient.new(MockBaseClient.new)
|
21
|
+
Prefab::FeatureFlagClient.send(:public, :is_on?)
|
22
|
+
|
23
|
+
flag = Prefab::FeatureFlag.new(feature: "FlagName", pct: 0)
|
24
|
+
|
25
|
+
assert_equal false,
|
26
|
+
client.is_on?(flag.feature, "hashes high", [], flag)
|
27
|
+
|
28
|
+
assert_equal false,
|
29
|
+
client.is_on?(flag.feature, "hashes low", [], flag)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def test_on
|
34
|
+
client = Prefab::FeatureFlagClient.new(MockBaseClient.new)
|
35
|
+
Prefab::FeatureFlagClient.send(:public, :is_on?)
|
36
|
+
|
37
|
+
flag = Prefab::FeatureFlag.new(feature: "FlagName", pct: 1)
|
38
|
+
|
39
|
+
assert_equal true,
|
40
|
+
client.is_on?(flag.feature, "hashes high", [], flag)
|
41
|
+
|
42
|
+
assert_equal true,
|
43
|
+
client.is_on?(flag.feature, "hashes low", [], flag)
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_whitelist
|
47
|
+
client = Prefab::FeatureFlagClient.new(MockBaseClient.new)
|
48
|
+
Prefab::FeatureFlagClient.send(:public, :is_on?)
|
49
|
+
|
50
|
+
flag = Prefab::FeatureFlag.new(feature: "FlagName", pct: 0, whitelisted: ["beta", "user:1", "user:3"])
|
51
|
+
|
52
|
+
assert_equal false,
|
53
|
+
client.is_on?(flag.feature, "anything", [], flag)
|
54
|
+
assert_equal true,
|
55
|
+
client.is_on?(flag.feature, "anything", ["beta"], flag)
|
56
|
+
assert_equal true,
|
57
|
+
client.is_on?(flag.feature, "anything", ["alpha", "beta"], flag)
|
58
|
+
assert_equal true,
|
59
|
+
client.is_on?(flag.feature, "anything", ["alpha", "user:1"], flag)
|
60
|
+
assert_equal false,
|
61
|
+
client.is_on?(flag.feature, "anything", ["alpha", "user:2"], flag)
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'prefab-cloud-ruby'
|
3
|
+
|
4
|
+
class MockBaseClient
|
5
|
+
attr_reader :namespace, :logger
|
6
|
+
|
7
|
+
def initialize(namespace: "")
|
8
|
+
@namespace = namespace
|
9
|
+
@logger = Logger.new($stdout)
|
10
|
+
end
|
11
|
+
|
12
|
+
def account_id
|
13
|
+
1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class MockConfigLoader
|
18
|
+
def calc_config
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prefab-cloud-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeff Dwyer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-01-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: grpc
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 1.
|
19
|
+
version: 1.8.3
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 1.
|
26
|
+
version: 1.8.3
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: concurrent-ruby
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -50,14 +50,14 @@ dependencies:
|
|
50
50
|
requirements:
|
51
51
|
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version:
|
53
|
+
version: 1.8.3
|
54
54
|
type: :development
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
58
|
- - "~>"
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version:
|
60
|
+
version: 1.8.3
|
61
61
|
- !ruby/object:Gem::Dependency
|
62
62
|
name: shoulda
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -154,10 +154,14 @@ files:
|
|
154
154
|
- lib/prefab/noop_stats.rb
|
155
155
|
- lib/prefab/ratelimit_client.rb
|
156
156
|
- lib/prefab/retry.rb
|
157
|
-
- lib/prefab/store.rb
|
158
157
|
- lib/prefab_pb.rb
|
159
158
|
- lib/prefab_services_pb.rb
|
160
159
|
- prefab-cloud-ruby.gemspec
|
160
|
+
- test/.prefab.test.config.yaml
|
161
|
+
- test/test_config_loader.rb
|
162
|
+
- test/test_config_resolver.rb
|
163
|
+
- test/test_feature_flag_client.rb
|
164
|
+
- test/test_helper.rb
|
161
165
|
homepage: http://github.com/prefab-cloud/prefab-cloud-ruby
|
162
166
|
licenses:
|
163
167
|
- MIT
|
data/lib/prefab/store.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
require "concurrent/atomics"
|
2
|
-
|
3
|
-
module EzConfig
|
4
|
-
class Store
|
5
|
-
def initialize
|
6
|
-
@store = Concurrent::Map
|
7
|
-
@lock = Concurrent::ReadWriteLock.new
|
8
|
-
end
|
9
|
-
|
10
|
-
def get(key)
|
11
|
-
@lock.with_read_lock do
|
12
|
-
@store[key]
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def all
|
17
|
-
@lock.with_read_lock do
|
18
|
-
@store.all
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def init(fs)
|
23
|
-
@lock.with_write_lock do
|
24
|
-
@store.replace(fs)
|
25
|
-
@initialized.make_true
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|