prefab-cloud-ruby 0.0.9 → 0.0.10
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.
- 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
|